File Coverage

blib/lib/DBIx/Class/AuditAny/Collector/AutoDBIC.pm
Criterion Covered Total %
statement 70 71 98.5
branch 11 18 61.1
condition 3 3 100.0
subroutine 15 15 100.0
pod 4 4 100.0
total 103 111 92.7


line stmt bran cond sub pod time code
1             package DBIx::Class::AuditAny::Collector::AutoDBIC;
2 11     11   7919 use strict;
  11         30  
  11         404  
3 11     11   71 use warnings;
  11         26  
  11         357  
4              
5             # ABSTRACT: Collector class for recording AuditAny changes in auto-generated DBIC schemas
6              
7 11     11   63 use Moo;
  11         27  
  11         77  
8 11     11   14702 use MooX::Types::MooseLike::Base qw(:all);
  11         36  
  11         4330  
9             extends 'DBIx::Class::AuditAny::Collector::DBIC';
10              
11             =head1 NAME
12              
13             DBIx::Class::AuditAny::Collector::AutoDBIC - Collector class for recording AuditAny
14             changes in auto-generated DBIC schemas
15              
16             =head1 DESCRIPTION
17              
18             This Collector facilitates recording ChangeSets, Changes, and Column Changes within a
19             clean relational structure into an automatically configured and deployed DBIC schema
20             using SQLite database files.
21              
22             This class extends L<DBIx::Class::AuditAny::Collector::DBIC> which provides greater
23             flexibility for configuration, can record to different forms of databases and tables,
24             and so on
25              
26             =head1 ATTRIBUTES
27              
28             Docs regarding the API/purpose of the attributes and methods in this class still TBD...
29              
30             =head2 auto_deploy
31              
32             =head2 change_data_rel
33              
34             =head2 change_source_name
35              
36             =head2 changeset_source_name
37              
38             =head2 column_change_source_name
39              
40             =head2 column_data_rel
41              
42             =head2 deploy_info_source_name
43              
44             =head2 reverse_change_data_rel
45              
46             =head2 reverse_changeset_data_rel
47              
48             =head2 sqlite_db
49              
50             =head1 METHODS
51              
52             =head2 get_context_column_infos
53              
54             =head2 init_schema_namespace
55              
56             =head2 deploy_schema
57              
58             =head2 get_clean_md5
59              
60             =cut
61              
62 11     11   102 use DBIx::Class::AuditAny::Util;
  11         38  
  11         1151  
63 11     11   5943 use DBIx::Class::AuditAny::Util::SchemaMaker;
  11         44  
  11         455  
64 11     11   6071 use String::CamelCase qw(decamelize);
  11         7593  
  11         774  
65 11     11   99 use Digest::MD5 qw(md5_hex);
  11         29  
  11         539  
66 11     11   73 use Data::Dumper;
  11         30  
  11         21499  
67              
68             has 'connect', is => 'ro', isa => ArrayRef, lazy => 1, default => sub {
69             my $self = shift;
70             my $db = $self->sqlite_db or die "no 'connect' or 'sqlite_db' specified.";
71             return [ "dbi:SQLite:dbname=$db","","", { AutoCommit => 1 } ];
72             };
73              
74             has 'sqlite_db', is => 'ro', isa => Maybe[Str], default => sub{undef};
75             has 'auto_deploy', is => 'ro', isa => Bool, default => sub{1};
76              
77             has 'target_schema_namespace', is => 'ro', lazy => 1, default => sub {
78             my $self = shift;
79             return ref($self->AuditObj->schema) . '::AuditSchema';
80             };
81              
82             has '+target_schema', default => sub {
83             my $self = shift;
84            
85             my $class = $self->init_schema_namespace;
86             my $schema = $class->connect(@{$self->connect});
87             $self->deploy_schema($schema) if ($self->auto_deploy);
88            
89             return $schema;
90             };
91              
92             has 'target_source', is => 'ro', isa => Str, lazy => 1,
93             default => sub { (shift)->changeset_source_name };
94              
95             has 'changeset_source_name', is => 'ro', isa => Str, default => sub{'AuditChangeSet'};
96             has 'change_source_name', is => 'ro', isa => Str, default => sub{'AuditChange'};
97             has 'column_change_source_name', is => 'ro', isa => Str, default => sub{'AuditChangeColumn'};
98             has 'deploy_info_source_name', is => 'ro', isa => Str, default => sub{'DeployInfo'};
99              
100             has 'changeset_table_name', is => 'ro', isa => Str, lazy => 1,
101             default => sub { decamelize((shift)->changeset_source_name) };
102            
103             has 'change_table_name', is => 'ro', isa => Str, lazy => 1,
104             default => sub { decamelize((shift)->change_source_name) };
105            
106             has 'column_change_table_name', is => 'ro', isa => Str, lazy => 1,
107             default => sub { decamelize((shift)->column_change_source_name) };
108              
109             has 'deploy_info_table_name', is => 'ro', isa => Str, lazy => 1,
110             default => sub { decamelize((shift)->deploy_info_source_name) };
111              
112             has '+change_data_rel', default => sub{'audit_changes'};
113             has '+column_data_rel', default => sub{'audit_change_columns'};
114             has 'reverse_change_data_rel', is => 'ro', isa => Str, default => sub{'change'};
115             has 'reverse_changeset_data_rel', is => 'ro', isa => Str, default => sub{'changeset'};
116              
117             has 'changeset_columns', is => 'ro', isa => ArrayRef, lazy => 1,
118             default => sub {
119             my $self = shift;
120             return [
121             id => {
122             data_type => "integer",
123             extra => { unsigned => 1 },
124             is_auto_increment => 1,
125             is_nullable => 0,
126             },
127             $self->get_context_column_infos(qw(base set))
128             ];
129             };
130              
131             has 'change_columns', is => 'ro', isa => ArrayRef, lazy => 1,
132             default => sub {
133             my $self = shift;
134             return [
135             id => {
136             data_type => "integer",
137             extra => { unsigned => 1 },
138             is_auto_increment => 1,
139             is_nullable => 0,
140             },
141             changeset_id => {
142             data_type => "integer",
143             extra => { unsigned => 1 },
144             is_foreign_key => 1,
145             is_nullable => 0,
146             },
147             $self->get_context_column_infos(qw(source change))
148             ];
149             };
150              
151             has 'change_column_columns', is => 'ro', isa => ArrayRef, lazy => 1,
152             default => sub {
153             my $self = shift;
154             return [
155             id => {
156             data_type => "integer",
157             extra => { unsigned => 1 },
158             is_auto_increment => 1,
159             is_nullable => 0,
160             },
161             change_id => {
162             data_type => "integer",
163             extra => { unsigned => 1 },
164             is_foreign_key => 1,
165             is_nullable => 0,
166             },
167             $self->get_context_column_infos(qw(column))
168             ];
169             };
170              
171             # Gets and validates DBIC column configs per supplied datapoint contexts
172             sub get_context_column_infos {
173 33     33 1 71 my $self = shift;
174 33         180 my @DataPoints = $self->AuditObj->get_context_datapoints(@_);
175 33 100       198 return () unless (scalar @DataPoints > 0);
176            
177 29         64 my %reserved = map {$_=>1} qw(id changeset_id change_id);
  87         266  
178 29         75 my %no_accessor = map {$_=>1} qw(new meta);
  58         151  
179            
180 29         71 my @cols = ();
181 29         78 foreach my $DataPoint (@DataPoints) {
182 118         363 my $name = $DataPoint->name;
183 118         1928 my $info = $DataPoint->column_info;
184 118 50       952 $reserved{$name} and die "Bad datapoint name '$name' - reserved keyword.";
185 118 100       256 $no_accessor{$name} and $info->{accessor} = undef;
186 118         284 push @cols, ( $name => $info );
187             }
188            
189 29         598 return @cols;
190             }
191              
192              
193             has 'schema_namespace_config', is => 'ro', isa => HashRef, init_arg => undef, lazy => 1,
194             default => sub {
195             my $self = shift;
196            
197             my $ColumnName = $self->AuditObj->get_datapoint_orig('column_name');
198             my $col_context_uniq_const = $ColumnName ?
199             [ add_unique_constraint => ["change_id", ["change_id", $ColumnName->name]] ] : [];
200              
201             my $namespace = $self->target_schema_namespace;
202             return {
203             schema_namespace => $namespace,
204             results => {
205             $self->deploy_info_source_name => {
206             table_name => $self->deploy_info_table_name,
207             columns => [
208             md5 => {
209             data_type => "char",
210             is_nullable => 0,
211             size => 32
212             },
213             comment => {
214             data_type => "varchar",
215             is_nullable => 0,
216             size => 255
217             },
218             deployed_ddl => {
219             data_type => 'mediumtext',
220             is_nullable => 0
221             },
222             deployed_ts => {
223             data_type => "datetime",
224             datetime_undef_if_invalid => 1,
225             is_nullable => 0
226             },
227             auditany_params => {
228             data_type => 'mediumtext',
229             is_nullable => 0
230             },
231             ],
232             call_class_methods => [
233             set_primary_key => ['md5'],
234             ]
235             },
236             $self->changeset_source_name => {
237             table_name => $self->changeset_table_name,
238             columns => $self->changeset_columns,
239             call_class_methods => [
240             set_primary_key => ['id'],
241             has_many => [
242             $self->change_data_rel,
243             $namespace . '::' . $self->change_source_name,
244             { "foreign.changeset_id" => "self.id" },
245             { cascade_copy => 0, cascade_delete => 0 },
246             ]
247             ]
248             },
249             $self->change_source_name => {
250             table_name => $self->change_table_name,
251             columns => $self->change_columns,
252             call_class_methods => [
253             set_primary_key => ['id'],
254             belongs_to => [
255             $self->reverse_changeset_data_rel,
256             $namespace . '::' . $self->changeset_source_name,
257             { id => "changeset_id" },
258             { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
259             ],
260             has_many => [
261             $self->column_data_rel,
262             $namespace . '::' . $self->column_change_source_name,
263             { "foreign.change_id" => "self.id" },
264             { cascade_copy => 0, cascade_delete => 0 },
265             ]
266             ]
267             },
268             $self->column_change_source_name => {
269             table_name => $self->column_change_table_name,
270             columns => $self->change_column_columns,
271             call_class_methods => [
272             set_primary_key => ['id'],
273             @$col_context_uniq_const,
274             #add_unique_constraint => ["change_id", ["change_id", "column_name"]],
275             belongs_to => [
276             $self->reverse_change_data_rel,
277             $namespace . '::' . $self->change_source_name,
278             { id => "change_id" },
279             { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
280             ],
281             ]
282             }
283             }
284             };
285             };
286              
287             sub init_schema_namespace {
288 11     11 1 24 my $self = shift;
289            
290             #scream($self->schema_namespace_config);
291            
292             return DBIx::Class::AuditAny::Util::SchemaMaker->initialize(
293 11         27 %{ $self->schema_namespace_config }
  11         188  
294             );
295             }
296              
297              
298             sub deploy_schema {
299 11     11 1 48 my $self = shift;
300 11         47 my $schema = shift;
301            
302 11         103 my $deploy_statements = $schema->deployment_statements;
303 11         1213413 my $md5 = $self->get_clean_md5($deploy_statements);
304 11         216 my $Rs = $schema->resultset($self->deploy_info_source_name);
305 11         10840 my $table = $Rs->result_source->from;
306 11         2479 my $deployRow;
307            
308             try {
309 11     11   597 $deployRow = $Rs->find($md5);
310             }
311             catch {
312             # Assume exception is due to not being deployed yet and try to deploy:
313 11     11   104326 $schema->deploy;
314            
315             # Save the actual AuditAny params, ->track() or ->new():
316 11         2325125 local $Data::Dumper::Maxdepth = 3;
317 11 50       609 my $auditany_params = $self->AuditObj->track_init_args ?
318             Data::Dumper->Dump([$self->AuditObj->track_init_args],['*track']) :
319             Data::Dumper->Dump([$self->AuditObj->build_init_args],['*new']);
320            
321 11         5078 $Rs->create({
322             md5 => $md5,
323             comment => 'DO NOT REMOVE THIS ROW',
324             deployed_ddl => $deploy_statements,
325             deployed_ts => $self->AuditObj->get_dt,
326             auditany_params => $auditany_params
327             });
328 11         137 };
329            
330             # If we've already been deployed and the ddl checksum matches:
331 11 50       271307 return 1 if ($deployRow);
332            
333 11         778 my $count = $Rs->count;
334 11         121518 my $dsn = $self->connect->[0];
335            
336 11 50       194 die "Database error; deploy_info table ('$table') exists but is empty in audit database '$dsn'"
337             unless ($count > 0);
338            
339 11 50       71 die "Database error; multiple rows in deploy_info table ('$table') in audit database '$dsn'"
340             if ($count > 1);
341            
342 11 50       77 my $exist_md5 = $Rs->first->md5 or die "Database error; found deploy_info row in table '$table' " .
343             "in audit database '$dsn', but it appears to be corrupt (no md5 checksum).";
344            
345 11 50       19297 return 1 if ($md5 eq $exist_md5);
346            
347 0         0 die "\n\n" . join("\n",
348             " The selected audit database '$dsn' already has a",
349             " deployed Collector::AutoDBIC schema (md5 checksum: $exist_md5) but it does",
350             " not match the current auto-generated schema (md5 checksum: $md5).",
351             " This probably means datapoints or other options have been changed since this AutoDBIC ",
352             " audit database was deployed. If you're not worried about existing audit logs, you can ",
353             " fix this error by simply clearing/deleting the audit database so it can be reinitialized."
354             ) . "\n\n";
355             }
356              
357             # Need to strip out comments and blank lines to make sure the md5s will be consistent
358             sub get_clean_md5 {
359 11     11 1 47 my $self = shift;
360 11         38 my $deploy_statements = shift;
361 11   100     548 my $clean = join("\n", grep { ! /^\-\-/ && ! /^\s*$/ } split(/\r?\n/,$deploy_statements) );
  679         2518  
362 11         244 return md5_hex($clean);
363             }
364              
365             1;
366              
367             __END__
368              
369             =head1 SEE ALSO
370              
371             =over
372              
373             =item *
374              
375             L<DBIx::Class::AuditAny>
376              
377             =item *
378              
379             L<DBIx::Class>
380              
381             =back
382              
383             =head1 SUPPORT
384            
385             IRC:
386            
387             Join #rapidapp on irc.perl.org.
388              
389             =head1 AUTHOR
390              
391             Henry Van Styn <vanstyn@cpan.org>
392              
393             =head1 COPYRIGHT AND LICENSE
394              
395             This software is copyright (c) 2012-2015 by IntelliTree Solutions llc.
396              
397             This is free software; you can redistribute it and/or modify it under
398             the same terms as the Perl 5 programming language system itself.
399              
400             =cut