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 10     10   6479 use strict;
  10         14  
  10         297  
3 10     10   37 use warnings;
  10         11  
  10         271  
4              
5             # ABSTRACT: Collector class for recording AuditAny changes in auto-generated DBIC schemas
6              
7 10     10   34 use Moo;
  10         13  
  10         55  
8 10     10   11641 use MooX::Types::MooseLike::Base qw(:all);
  10         14  
  10         3512  
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 10     10   49 use DBIx::Class::AuditAny::Util;
  10         11  
  10         777  
63 10     10   4110 use DBIx::Class::AuditAny::Util::SchemaMaker;
  10         19  
  10         331  
64 10     10   4343 use String::CamelCase qw(decamelize);
  10         4458  
  10         583  
65 10     10   49 use Digest::MD5 qw(md5_hex);
  10         12  
  10         363  
66 10     10   50 use Data::Dumper;
  10         14  
  10         13166  
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 30     30 1 38 my $self = shift;
174 30         126 my @DataPoints = $self->AuditObj->get_context_datapoints(@_);
175 30 100       149 return () unless (scalar @DataPoints > 0);
176            
177 26         33 my %reserved = map {$_=>1} qw(id changeset_id change_id);
  78         132  
178 26         40 my %no_accessor = map {$_=>1} qw(new meta);
  52         81  
179            
180 26         39 my @cols = ();
181 26         44 foreach my $DataPoint (@DataPoints) {
182 107         181 my $name = $DataPoint->name;
183 107         1444 my $info = $DataPoint->column_info;
184 107 50       489 $reserved{$name} and die "Bad datapoint name '$name' - reserved keyword.";
185 107 100       152 $no_accessor{$name} and $info->{accessor} = undef;
186 107         153 push @cols, ( $name => $info );
187             }
188            
189 26         426 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 10     10 1 16 my $self = shift;
289            
290             #scream($self->schema_namespace_config);
291            
292             return DBIx::Class::AuditAny::Util::SchemaMaker->initialize(
293 10         23 %{ $self->schema_namespace_config }
  10         191  
294             );
295             }
296              
297              
298             sub deploy_schema {
299 10     10 1 20 my $self = shift;
300 10         18 my $schema = shift;
301            
302 10         74 my $deploy_statements = $schema->deployment_statements;
303 10         775124 my $md5 = $self->get_clean_md5($deploy_statements);
304 10         181 my $Rs = $schema->resultset($self->deploy_info_source_name);
305 10         7077 my $table = $Rs->result_source->from;
306 10         1424 my $deployRow;
307            
308             try {
309 10     10   334 $deployRow = $Rs->find($md5);
310             }
311             catch {
312             # Assume exception is due to not being deployed yet and try to deploy:
313 10     10   92917 $schema->deploy;
314            
315             # Save the actual AuditAny params, ->track() or ->new():
316 10         1442990 local $Data::Dumper::Maxdepth = 3;
317 10 50       409 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 10         3216 $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 10         102 };
329            
330             # If we've already been deployed and the ddl checksum matches:
331 10 50       163189 return 1 if ($deployRow);
332            
333 10         797 my $count = $Rs->count;
334 10         75475 my $dsn = $self->connect->[0];
335            
336 10 50       207 die "Database error; deploy_info table ('$table') exists but is empty in audit database '$dsn'"
337             unless ($count > 0);
338            
339 10 50       95 die "Database error; multiple rows in deploy_info table ('$table') in audit database '$dsn'"
340             if ($count > 1);
341            
342 10 50       54 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 10 50       12057 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 10     10 1 24 my $self = shift;
360 10         19 my $deploy_statements = shift;
361 10   100     402 my $clean = join("\n", grep { ! /^\-\-/ && ! /^\s*$/ } split(/\r?\n/,$deploy_statements) );
  617         1577  
362 10         142 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