line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package DBIx::Class::AuditAny; |
2
|
14
|
|
|
14
|
|
9288876
|
use strict; |
|
14
|
|
|
|
|
39
|
|
|
14
|
|
|
|
|
493
|
|
3
|
14
|
|
|
14
|
|
92
|
use warnings; |
|
14
|
|
|
|
|
31
|
|
|
14
|
|
|
|
|
668
|
|
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
# ABSTRACT: Flexible change tracking framework for DBIx::Class |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
our $VERSION = '0.200200'; |
8
|
|
|
|
|
|
|
|
9
|
14
|
|
|
14
|
|
352
|
use 5.010; |
|
14
|
|
|
|
|
66
|
|
10
|
|
|
|
|
|
|
|
11
|
14
|
|
|
14
|
|
1942
|
use Moo; |
|
14
|
|
|
|
|
35956
|
|
|
14
|
|
|
|
|
143
|
|
12
|
14
|
|
|
14
|
|
37677
|
use MooX::Types::MooseLike::Base 0.19 qw(:all); |
|
14
|
|
|
|
|
100468
|
|
|
14
|
|
|
|
|
5007
|
|
13
|
|
|
|
|
|
|
|
14
|
14
|
|
|
14
|
|
3878
|
use Class::MOP; |
|
14
|
|
|
|
|
693292
|
|
|
14
|
|
|
|
|
653
|
|
15
|
14
|
|
|
14
|
|
127
|
use Class::MOP::Class; |
|
14
|
|
|
|
|
57
|
|
|
14
|
|
|
|
|
564
|
|
16
|
14
|
|
|
14
|
|
15388
|
use DateTime; |
|
14
|
|
|
|
|
6893981
|
|
|
14
|
|
|
|
|
835
|
|
17
|
14
|
|
|
14
|
|
9470
|
use DBIx::Class::AuditAny::Util; |
|
14
|
|
|
|
|
46
|
|
|
14
|
|
|
|
|
1276
|
|
18
|
14
|
|
|
14
|
|
7205
|
use DBIx::Class::AuditAny::Util::BuiltinDatapoints; |
|
14
|
|
|
|
|
43
|
|
|
14
|
|
|
|
|
499
|
|
19
|
14
|
|
|
14
|
|
7119
|
use DBIx::Class::AuditAny::Role::Schema; |
|
14
|
|
|
|
|
49
|
|
|
14
|
|
|
|
|
586
|
|
20
|
|
|
|
|
|
|
|
21
|
14
|
|
|
14
|
|
118
|
use Term::ANSIColor qw(:constants); |
|
14
|
|
|
|
|
34
|
|
|
14
|
|
|
|
|
69369
|
|
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
has 'time_zone', is => 'ro', isa => Str, default => sub{'local'}; |
24
|
205
|
|
|
205
|
1
|
1773
|
sub get_dt { DateTime->now( time_zone => (shift)->time_zone ) } |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
has 'schema', is => 'ro', required => 1, isa => InstanceOf['DBIx::Class::Schema']; #<--- This won't go back to Moose |
27
|
|
|
|
|
|
|
has 'track_immutable', is => 'ro', isa => Bool, default => sub{0}; |
28
|
|
|
|
|
|
|
has 'track_actions', is => 'ro', isa => ArrayRef, default => sub { [qw(insert update delete)] }; |
29
|
|
|
|
|
|
|
has 'allow_multiple_auditors', is => 'ro', isa => Bool, default => sub{0}; |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
has 'source_context_class', is => 'ro', default => sub{'AuditContext::Source'}; |
32
|
|
|
|
|
|
|
has 'change_context_class', is => 'ro', default => sub{'AuditContext::Change'}; |
33
|
|
|
|
|
|
|
has 'changeset_context_class', is => 'ro', default => sub{'AuditContext::ChangeSet'}; |
34
|
|
|
|
|
|
|
has 'column_context_class', is => 'ro', default => sub{'AuditContext::Column'}; |
35
|
|
|
|
|
|
|
has 'default_datapoint_class', is => 'ro', default => sub{'DataPoint'}; |
36
|
|
|
|
|
|
|
has 'collector_class', is => 'ro', isa => Str; |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
around $_ => sub { |
39
|
|
|
|
|
|
|
my $orig = shift; my $self = shift; |
40
|
|
|
|
|
|
|
resolve_localclass $self->$orig(@_); |
41
|
|
|
|
|
|
|
} for qw( |
42
|
|
|
|
|
|
|
source_context_class change_context_class |
43
|
|
|
|
|
|
|
changeset_context_class column_context_class |
44
|
|
|
|
|
|
|
default_datapoint_class collector_class |
45
|
|
|
|
|
|
|
); |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
has 'collector_params', is => 'ro', isa => HashRef, default => sub {{}}; |
48
|
|
|
|
|
|
|
has 'primary_key_separator', is => 'ro', isa => Str, default => sub{'|~|'}; |
49
|
|
|
|
|
|
|
has 'datapoint_configs', is => 'ro', isa => ArrayRef[HashRef], default => sub {[]}; |
50
|
|
|
|
|
|
|
has 'auto_include_user_defined_datapoints', is => 'ro', isa => Bool, default => sub{1}; |
51
|
|
|
|
|
|
|
has 'rename_datapoints', is => 'ro', isa => Maybe[HashRef[Str]], default => sub{undef}; |
52
|
|
|
|
|
|
|
has 'disable_datapoints', is => 'ro', isa => ArrayRef, default => sub {[]}; |
53
|
|
|
|
|
|
|
has 'record_empty_changes', is => 'ro', isa => Bool, default => sub{0}; |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
has 'datapoints', is => 'ro', isa => ArrayRef[Str], |
56
|
|
|
|
|
|
|
default => sub{[qw( |
57
|
|
|
|
|
|
|
change_ts |
58
|
|
|
|
|
|
|
action |
59
|
|
|
|
|
|
|
source |
60
|
|
|
|
|
|
|
pri_key_value |
61
|
|
|
|
|
|
|
column_name |
62
|
|
|
|
|
|
|
old_value |
63
|
|
|
|
|
|
|
new_value |
64
|
|
|
|
|
|
|
)]}; |
65
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
has 'collector', is => 'ro', lazy => 1, default => sub { |
67
|
|
|
|
|
|
|
my $self = shift; |
68
|
|
|
|
|
|
|
return ($self->collector_class)->new( |
69
|
|
|
|
|
|
|
%{$self->collector_params}, |
70
|
|
|
|
|
|
|
AuditObj => $self |
71
|
|
|
|
|
|
|
); |
72
|
|
|
|
|
|
|
}; |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
# Any sources within the tracked schema that the collector is writing to; these |
75
|
|
|
|
|
|
|
# sources are not allowed to be tracked because it would create infinite recursion: |
76
|
|
|
|
|
|
|
has 'log_sources', is => 'ro', isa => ArrayRef[Str], lazy => 1, init_arg => undef, default => sub { |
77
|
|
|
|
|
|
|
my $self = shift; |
78
|
|
|
|
|
|
|
return $self->collector->writes_bound_schema_sources; |
79
|
|
|
|
|
|
|
}; |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
has 'tracked_action_functions', is => 'ro', isa => HashRef, default => sub {{}}; |
82
|
|
|
|
|
|
|
has 'tracked_sources', is => 'ro', isa => HashRef[Str], default => sub {{}}; |
83
|
|
|
|
|
|
|
has 'calling_action_function', is => 'ro', isa => HashRef[Bool], default => sub {{}}; |
84
|
|
|
|
|
|
|
has 'active_changeset', is => 'rw', isa => Maybe[Object], default => sub{undef}; |
85
|
|
|
|
|
|
|
has 'auto_finish', is => 'rw', isa => Bool, default => sub{0}; |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
has 'track_init_args', is => 'ro', isa => Maybe[HashRef], default => sub{undef}; |
88
|
|
|
|
|
|
|
has 'build_init_args', is => 'ro', isa => HashRef, required => 1; |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
around BUILDARGS => sub { |
91
|
|
|
|
|
|
|
my $orig = shift; |
92
|
|
|
|
|
|
|
my $class = shift; |
93
|
|
|
|
|
|
|
my %opts = (ref($_[0]) eq 'HASH') ? %{ $_[0] } : @_; # <-- arg as hash or hashref |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
die 'Cannot specify build_init_args in new()' if (exists $opts{build_init_args}); |
96
|
|
|
|
|
|
|
$opts{build_init_args} = { %opts }; |
97
|
|
|
|
|
|
|
return $class->$orig(%opts); |
98
|
|
|
|
|
|
|
}; |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
sub track { |
101
|
15
|
|
|
15
|
1
|
3067267
|
my $class = shift; |
102
|
15
|
50
|
|
|
|
422
|
my %opts = (ref($_[0]) eq 'HASH') ? %{ $_[0] } : @_; # <-- arg as hash or hashref |
|
0
|
|
|
|
|
0
|
|
103
|
15
|
50
|
|
|
|
86
|
die "track cannot be called on object instances" if (ref $class); |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
# Record the track init arguments: |
106
|
15
|
|
|
|
|
87
|
$opts{track_init_args} = { %opts }; |
107
|
|
|
|
|
|
|
|
108
|
15
|
50
|
|
|
|
76
|
my $sources = exists $opts{track_sources} ? delete $opts{track_sources} : undef; |
109
|
15
|
50
|
33
|
|
|
74
|
die 'track_sources must be an arrayref' if ($sources and ! ref($sources) eq 'ARRAY'); |
110
|
15
|
50
|
|
|
|
66
|
my $track_all = exists $opts{track_all_sources} ? delete $opts{track_all_sources} : undef; |
111
|
15
|
50
|
33
|
|
|
74
|
die "track_sources and track_all_sources are incompatible" if ($sources && $track_all); |
112
|
|
|
|
|
|
|
|
113
|
15
|
50
|
|
|
|
59
|
my $init_sources = exists $opts{init_sources} ? delete $opts{init_sources} : undef; |
114
|
15
|
50
|
33
|
|
|
92
|
die 'init_sources must be an arrayref' if ($init_sources and ! ref($init_sources) eq 'ARRAY'); |
115
|
15
|
50
|
|
|
|
76
|
my $init_all = exists $opts{init_all_sources} ? delete $opts{init_all_sources} : undef; |
116
|
15
|
50
|
33
|
|
|
70
|
die "init_sources and init_all_sources are incompatible" if ($init_sources && $init_all); |
117
|
|
|
|
|
|
|
|
118
|
15
|
100
|
|
|
|
53
|
my $collect = exists $opts{collect} ? delete $opts{collect} : undef; |
119
|
15
|
100
|
|
|
|
51
|
if ($collect) { |
120
|
|
|
|
|
|
|
die "'collect' cannot be used with 'collector_params', 'collector_class' or 'collector'" |
121
|
2
|
50
|
33
|
|
|
21
|
if ($opts{collector_params} || $opts{collector_class} || $opts{collector}); |
|
|
|
33
|
|
|
|
|
122
|
|
|
|
|
|
|
|
123
|
2
|
|
|
|
|
5
|
$opts{collector_class} = 'Collector::Code'; |
124
|
2
|
|
|
|
|
7
|
$opts{collector_params} = { collect_coderef => $collect }; |
125
|
|
|
|
|
|
|
} |
126
|
|
|
|
|
|
|
|
127
|
15
|
50
|
|
|
|
56
|
if($opts{collector}) { |
128
|
|
|
|
|
|
|
die "'collector' cannot be used with 'collector_params', 'collector_class' or 'collect'" |
129
|
0
|
0
|
0
|
|
|
0
|
if ($opts{collector_params} || $opts{collector_class} || $opts{collect}); |
|
|
|
0
|
|
|
|
|
130
|
|
|
|
|
|
|
} |
131
|
|
|
|
|
|
|
|
132
|
15
|
|
|
|
|
114
|
my $self = $class->new(%opts); |
133
|
|
|
|
|
|
|
|
134
|
15
|
50
|
|
|
|
5365
|
$self->track_sources(@$sources) if ($sources); |
135
|
15
|
50
|
|
|
|
142
|
$self->track_all_sources if ($track_all); |
136
|
|
|
|
|
|
|
|
137
|
15
|
50
|
|
|
|
88
|
$self->init_sources(@$init_sources) if ($init_sources); |
138
|
15
|
50
|
|
|
|
72
|
$self->init_all_sources if ($init_all); |
139
|
15
|
|
|
|
|
211
|
return $self; |
140
|
|
|
|
|
|
|
} |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
sub _get_datapoint_configs { |
144
|
15
|
|
|
15
|
|
41
|
my $self = shift; |
145
|
|
|
|
|
|
|
|
146
|
15
|
|
|
|
|
189
|
my @configs = DBIx::Class::AuditAny::Util::BuiltinDatapoints->all_configs; |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
# strip out any being redefined: |
149
|
15
|
|
|
|
|
80
|
my %cust = map {$_->{name}=>1} @{$self->datapoint_configs}; |
|
4
|
|
|
|
|
19
|
|
|
15
|
|
|
|
|
124
|
|
150
|
15
|
|
|
|
|
53
|
@configs = grep { !$cust{$_->{name}} } @configs; |
|
330
|
|
|
|
|
650
|
|
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
# Set flag to mark the configs that were user defined |
153
|
15
|
|
|
|
|
89
|
$_->{user_defined} = 1 for (@{$self->datapoint_configs}); |
|
15
|
|
|
|
|
108
|
|
154
|
|
|
|
|
|
|
|
155
|
15
|
|
|
|
|
58
|
push @configs, @{$self->datapoint_configs}; |
|
15
|
|
|
|
|
75
|
|
156
|
|
|
|
|
|
|
|
157
|
15
|
|
|
|
|
78
|
return @configs; |
158
|
|
|
|
|
|
|
} |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
has '_datapoints', is => 'ro', isa => HashRef, default => sub {{}}; |
161
|
|
|
|
|
|
|
has '_datapoints_context', is => 'ro', isa => HashRef, default => sub {{}}; |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
# Also index datapoints by 'original_name' which will be different from 'name' |
164
|
|
|
|
|
|
|
# whenever 'rename_datapoints' has been applied |
165
|
|
|
|
|
|
|
has '_datapoints_orig_names', is => 'ro', isa => HashRef, default => sub {{}}; |
166
|
11
|
|
|
11
|
1
|
86
|
sub get_datapoint_orig { (shift)->_datapoints_orig_names->{(shift)} } |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
sub add_datapoints { |
169
|
154
|
|
|
154
|
1
|
316
|
my $self = shift; |
170
|
154
|
|
|
|
|
3298
|
my $class = $self->default_datapoint_class; |
171
|
154
|
|
|
|
|
482
|
foreach my $cnf (@_) { |
172
|
154
|
50
|
|
|
|
426
|
die "'$cnf' not expected ref" unless (ref $cnf); |
173
|
154
|
50
|
|
|
|
659
|
$class = delete $cnf->{class} if ($cnf->{class}); |
174
|
154
|
50
|
|
|
|
3189
|
my $DataPoint = ref($cnf) eq $class ? $cnf : $class->new($cnf); |
175
|
154
|
50
|
|
|
|
6156
|
die "Error creating datapoint object" unless (ref($DataPoint) eq $class); |
176
|
154
|
50
|
|
|
|
789
|
die "Duplicate datapoint name '" . $DataPoint->name . "'" if ($self->_datapoints->{$DataPoint->name}); |
177
|
154
|
|
|
|
|
521
|
$self->_datapoints->{$DataPoint->name} = $DataPoint; |
178
|
154
|
|
|
|
|
588
|
$self->_datapoints_context->{$DataPoint->context}->{$DataPoint->name} = $DataPoint; |
179
|
154
|
|
|
|
|
2929
|
$self->_datapoints_orig_names->{$DataPoint->original_name} = $DataPoint; |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
} |
182
|
0
|
|
|
0
|
1
|
0
|
sub all_datapoints { values %{(shift)->_datapoints} } |
|
0
|
|
|
|
|
0
|
|
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
sub get_context_datapoints { |
185
|
497
|
|
|
497
|
1
|
934
|
my $self = shift; |
186
|
497
|
|
|
|
|
973
|
my @contexts = grep { exists $self->_datapoints_context->{$_} } @_; |
|
545
|
|
|
|
|
2485
|
|
187
|
497
|
|
|
|
|
1178
|
return map { values %{$self->_datapoints_context->{$_}} } @contexts; |
|
517
|
|
|
|
|
748
|
|
|
517
|
|
|
|
|
2707
|
|
188
|
|
|
|
|
|
|
} |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
sub get_context_datapoint_names { |
191
|
3
|
|
|
3
|
1
|
10
|
my $self = shift; |
192
|
3
|
|
|
|
|
13
|
return map { $_->name } $self->get_context_datapoints(@_); |
|
9
|
|
|
|
|
32
|
|
193
|
|
|
|
|
|
|
} |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
|
196
|
104
|
|
|
104
|
1
|
2853
|
sub local_datapoint_data { (shift)->base_datapoint_values } |
197
|
|
|
|
|
|
|
has 'base_datapoint_values', is => 'ro', isa => HashRef, lazy => 1, default => sub { |
198
|
|
|
|
|
|
|
my $self = shift; |
199
|
|
|
|
|
|
|
return { map { $_->name => $_->get_value($self) } $self->get_context_datapoints('base') }; |
200
|
|
|
|
|
|
|
}; |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
sub _init_datapoints { |
203
|
15
|
|
|
15
|
|
45
|
my $self = shift; |
204
|
|
|
|
|
|
|
|
205
|
15
|
|
|
|
|
106
|
my @configs = $self->_get_datapoint_configs; |
206
|
|
|
|
|
|
|
|
207
|
15
|
100
|
|
|
|
128
|
if($self->rename_datapoints) { |
208
|
8
|
|
|
|
|
30
|
my $rename = $self->rename_datapoints; |
209
|
|
|
|
|
|
|
|
210
|
8
|
100
|
|
|
|
25
|
@{$self->datapoints} = map { $rename->{$_} || $_ } @{$self->datapoints}; |
|
8
|
|
|
|
|
41
|
|
|
86
|
|
|
|
|
319
|
|
|
8
|
|
|
|
|
54
|
|
211
|
|
|
|
|
|
|
|
212
|
8
|
|
|
|
|
31
|
foreach my $cnf (@configs) { |
213
|
180
|
100
|
|
|
|
400
|
next unless (exists $rename->{$cnf->{name}}); |
214
|
48
|
|
|
|
|
102
|
$cnf->{original_name} = $cnf->{name}; |
215
|
48
|
|
|
|
|
107
|
$cnf->{name} = $rename->{$cnf->{name}}; |
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
} |
218
|
|
|
|
|
|
|
|
219
|
15
|
|
|
|
|
55
|
my %seen = (); |
220
|
15
|
|
50
|
|
|
88
|
$seen{$_}++ and die "Duplicate datapoint name '$_'" for (@{$self->datapoints}); |
|
15
|
|
|
|
|
332
|
|
221
|
|
|
|
|
|
|
|
222
|
15
|
|
|
|
|
52
|
my %disable = map {$_=>1} @{$self->disable_datapoints}; |
|
0
|
|
|
|
|
0
|
|
|
15
|
|
|
|
|
102
|
|
223
|
15
|
|
|
|
|
43
|
my %activ = map {$_=>1} grep { !$disable{$_} } @{$self->datapoints}; |
|
150
|
|
|
|
|
356
|
|
|
150
|
|
|
|
|
336
|
|
|
15
|
|
|
|
|
84
|
|
224
|
|
|
|
|
|
|
|
225
|
15
|
50
|
|
|
|
151
|
if($self->auto_include_user_defined_datapoints) { |
226
|
15
|
50
|
|
|
|
54
|
$activ{$_->{name}} = 1 for(grep { $_->{name} && $_->{user_defined} } @configs); |
|
334
|
|
|
|
|
1204
|
|
227
|
|
|
|
|
|
|
} |
228
|
|
|
|
|
|
|
|
229
|
15
|
|
|
|
|
63
|
foreach my $cnf (@configs) { |
230
|
|
|
|
|
|
|
# Do this just to throw the exception for no name: |
231
|
334
|
50
|
|
|
|
4736
|
$self->add_datapoints($cnf) unless ($cnf->{name}); |
232
|
|
|
|
|
|
|
|
233
|
334
|
100
|
|
|
|
1055
|
next unless $activ{$cnf->{name}}; |
234
|
154
|
|
|
|
|
376
|
delete $activ{$cnf->{name}}; |
235
|
154
|
|
|
|
|
1021
|
$self->add_datapoints({%$cnf, AuditObj => $self}); |
236
|
|
|
|
|
|
|
} |
237
|
|
|
|
|
|
|
|
238
|
15
|
50
|
|
|
|
844
|
die "Unknown datapoint(s) specified (" . join(',',keys %activ) . ')' |
239
|
|
|
|
|
|
|
if (scalar(keys %activ) > 0); |
240
|
|
|
|
|
|
|
} |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
sub BUILD { |
244
|
15
|
|
|
15
|
0
|
834
|
my $self = shift; |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
# init all classes first: |
247
|
15
|
|
|
|
|
361
|
$self->change_context_class; |
248
|
15
|
|
|
|
|
485
|
$self->changeset_context_class; |
249
|
15
|
|
|
|
|
490
|
$self->source_context_class; |
250
|
15
|
|
|
|
|
498
|
$self->column_context_class; |
251
|
15
|
|
|
|
|
481
|
$self->default_datapoint_class; |
252
|
|
|
|
|
|
|
|
253
|
15
|
|
|
|
|
126
|
$self->_init_datapoints; |
254
|
15
|
|
|
|
|
92
|
$self->_bind_schema; |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
# init collector object: |
257
|
15
|
|
|
|
|
635
|
$self->collector; |
258
|
|
|
|
|
|
|
} |
259
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
sub _init_apply_schema_class { |
262
|
15
|
|
|
15
|
|
41
|
my $self = shift; |
263
|
15
|
50
|
|
|
|
166
|
die "schema is not a reference" unless (ref $self->schema); |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
Moo::Role->apply_roles_to_object($self->schema,'DBIx::Class::AuditAny::Role::Schema') |
266
|
15
|
50
|
|
15
|
|
177
|
unless try{$self->schema->does('DBIx::Class::AuditAny::Role::Schema')}; |
|
15
|
|
|
|
|
942
|
|
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
# Important! |
269
|
15
|
|
|
|
|
60047
|
$self->schema->_apply_storage_role; |
270
|
|
|
|
|
|
|
} |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
sub _bind_schema { |
277
|
15
|
|
|
15
|
|
47
|
my $self = shift; |
278
|
15
|
|
|
|
|
108
|
$self->_init_apply_schema_class; |
279
|
|
|
|
|
|
|
|
280
|
15
|
50
|
33
|
|
|
111570
|
die "Supplied Schema instance already has a bound Auditor - to allow multple " . |
281
|
|
|
|
|
|
|
"Auditors, set 'allow_multiple_auditors' to true" |
282
|
|
|
|
|
|
|
if($self->schema->auditor_count > 0 and ! $self->allow_multiple_auditors); |
283
|
|
|
|
|
|
|
|
284
|
15
|
|
50
|
|
|
166
|
$_ == $self and return for($self->schema->auditors); |
285
|
|
|
|
|
|
|
|
286
|
15
|
|
|
|
|
870
|
return $self->schema->add_auditor($self); |
287
|
|
|
|
|
|
|
} |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
sub track_sources { |
293
|
15
|
|
|
15
|
1
|
79
|
my ($self,@sources) = @_; |
294
|
|
|
|
|
|
|
|
295
|
15
|
|
|
|
|
69
|
foreach my $name (@sources) { |
296
|
95
|
50
|
|
|
|
666
|
my $Source = $self->schema->source($name) or die "Bad Result Source name '$name'"; |
297
|
|
|
|
|
|
|
|
298
|
95
|
|
|
|
|
6590
|
my $class = $self->source_context_class; |
299
|
95
|
|
|
|
|
1781
|
my $AuditSourceContext = $class->new( |
300
|
|
|
|
|
|
|
AuditObj => $self, |
301
|
|
|
|
|
|
|
ResultSource => $Source |
302
|
|
|
|
|
|
|
); |
303
|
|
|
|
|
|
|
|
304
|
95
|
|
|
|
|
54713
|
my $source_name = $AuditSourceContext->source; |
305
|
|
|
|
|
|
|
|
306
|
95
|
|
|
|
|
193
|
my %log_sources = map {$_=>1} @{$self->log_sources}; |
|
9
|
|
|
|
|
42
|
|
|
95
|
|
|
|
|
1565
|
|
307
|
|
|
|
|
|
|
die "The Log Source (" . $source_name . ") cannot track itself!!" |
308
|
95
|
50
|
|
|
|
872
|
if ($log_sources{$source_name}); |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
# Skip sources we've already setup: |
311
|
95
|
50
|
|
|
|
342
|
return if ($self->tracked_sources->{$source_name}); |
312
|
|
|
|
|
|
|
|
313
|
95
|
|
|
|
|
319
|
$self->_add_row_trackers_methods($AuditSourceContext); |
314
|
95
|
|
|
|
|
624
|
$self->tracked_sources->{$source_name} = $AuditSourceContext; |
315
|
|
|
|
|
|
|
} |
316
|
|
|
|
|
|
|
} |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
sub track_all_sources { |
319
|
15
|
|
|
15
|
1
|
69
|
my ($self,@exclude) = @_; |
320
|
|
|
|
|
|
|
#$class->_init; |
321
|
|
|
|
|
|
|
|
322
|
15
|
|
|
|
|
58
|
push @exclude, @{$self->log_sources}; |
|
15
|
|
|
|
|
383
|
|
323
|
|
|
|
|
|
|
|
324
|
15
|
|
|
|
|
2746
|
my %excl = map {$_=>1} @exclude; |
|
3
|
|
|
|
|
11
|
|
325
|
15
|
|
|
|
|
396
|
return $self->track_sources(grep { !$excl{$_} } $self->schema->sources); |
|
98
|
|
|
|
|
1033
|
|
326
|
|
|
|
|
|
|
} |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
# This is the original, Row-based solution for initializing existing data. This |
329
|
|
|
|
|
|
|
# is going to be refactored and replaced, but with what has not been decided yet |
330
|
|
|
|
|
|
|
# See also _add_additional_row_methods() below |
331
|
|
|
|
|
|
|
sub init_sources { |
332
|
0
|
|
|
0
|
1
|
0
|
my ($self,@sources) = @_; |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
$self->schema->txn_do(sub { |
335
|
|
|
|
|
|
|
|
336
|
0
|
|
|
0
|
|
0
|
foreach my $name (@sources) { |
337
|
0
|
0
|
|
|
|
0
|
my $SourceContext = $self->tracked_sources->{$name} |
338
|
|
|
|
|
|
|
or die "Source '$name' is not being tracked"; |
339
|
|
|
|
|
|
|
|
340
|
0
|
|
|
|
|
0
|
print STDERR "\n"; |
341
|
|
|
|
|
|
|
|
342
|
0
|
|
|
|
|
0
|
my $msg = "Initializing Audit Records for $name: "; |
343
|
0
|
|
|
|
|
0
|
print STDERR $msg . "\r"; |
344
|
|
|
|
|
|
|
|
345
|
0
|
|
|
|
|
0
|
my $Rs = $SourceContext->ResultSource->resultset; |
346
|
0
|
|
|
|
|
0
|
my $total = $Rs->count; |
347
|
0
|
|
|
|
|
0
|
my $count = 0; |
348
|
0
|
|
|
|
|
0
|
foreach my $Row ($Rs->all) { |
349
|
0
|
|
|
|
|
0
|
print STDERR $msg . ++$count . '/' . $total . "\r"; |
350
|
0
|
|
|
|
|
0
|
$Row->audit_init($self); |
351
|
|
|
|
|
|
|
} |
352
|
|
|
|
|
|
|
} |
353
|
|
|
|
|
|
|
|
354
|
0
|
|
|
|
|
0
|
print STDERR "\n\n"; |
355
|
0
|
|
|
|
|
0
|
}); |
356
|
|
|
|
|
|
|
} |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
sub init_all_sources { |
359
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
360
|
0
|
|
|
|
|
0
|
$self->init_sources(keys %{$self->tracked_sources}); |
|
0
|
|
|
|
|
0
|
|
361
|
|
|
|
|
|
|
} |
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
our $NESTED_CALL = 0; |
365
|
|
|
|
|
|
|
sub _add_row_trackers_methods { |
366
|
95
|
|
|
95
|
|
165
|
my $self = shift; |
367
|
95
|
|
|
|
|
155
|
my $AuditSourceContext = shift; |
368
|
|
|
|
|
|
|
|
369
|
95
|
|
|
|
|
1505
|
my $source_name = $AuditSourceContext->source; |
370
|
95
|
|
|
|
|
1207
|
my $result_class = $self->schema->class($source_name); |
371
|
|
|
|
|
|
|
|
372
|
95
|
|
|
|
|
7569
|
foreach my $action (@{$self->track_actions}) { |
|
95
|
|
|
|
|
331
|
|
373
|
285
|
|
|
|
|
607
|
my $func_name = $source_name . '::' . $action; |
374
|
285
|
50
|
|
|
|
1146
|
return if $self->tracked_action_functions->{$func_name}++; |
375
|
|
|
|
|
|
|
} |
376
|
|
|
|
|
|
|
|
377
|
95
|
|
|
|
|
282
|
$self->_add_additional_row_methods($result_class); |
378
|
|
|
|
|
|
|
} |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
# TODO/FIXME: This needs to be refactored to use a cleaner API. Probably |
383
|
|
|
|
|
|
|
# totally different (this code is leftover from before the switch to the |
384
|
|
|
|
|
|
|
# Storage Role API) |
385
|
|
|
|
|
|
|
sub _add_additional_row_methods { |
386
|
95
|
|
|
95
|
|
165
|
my $self = shift; |
387
|
95
|
|
|
|
|
165
|
my $result_class = shift; |
388
|
|
|
|
|
|
|
|
389
|
95
|
|
|
|
|
506
|
my $meta = Class::MOP::Class->initialize($result_class); |
390
|
95
|
|
|
|
|
58881
|
my $immutable = $meta->is_immutable; |
391
|
|
|
|
|
|
|
|
392
|
95
|
50
|
33
|
|
|
473
|
die "Won't add tracker/modifier method to immutable Result Class '$result_class' " . |
393
|
|
|
|
|
|
|
'(hint: did you forget to remove __PACKAGE__->meta->make_immutable ??)' . |
394
|
|
|
|
|
|
|
' - to force/override, set "track_immutable" to true.' |
395
|
|
|
|
|
|
|
if ($immutable && !$self->track_immutable); |
396
|
|
|
|
|
|
|
|
397
|
|
|
|
|
|
|
# Tempory turn mutable back on, saving any immutable_options, first: |
398
|
95
|
|
|
|
|
197
|
my %immut_opts = (); |
399
|
95
|
50
|
|
|
|
227
|
if($immutable) { |
400
|
0
|
|
|
|
|
0
|
%immut_opts = $meta->immutable_options; |
401
|
0
|
|
|
|
|
0
|
$meta->make_mutable; |
402
|
|
|
|
|
|
|
} |
403
|
|
|
|
|
|
|
|
404
|
95
|
100
|
|
|
|
541
|
return if ($meta->has_method('audit_take_snapshot')); |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
$meta->add_method( audit_take_snapshot => sub { |
407
|
0
|
|
|
0
|
|
0
|
my $Row = shift; |
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
408
|
0
|
0
|
|
|
|
0
|
my $AuditObj = shift or die "AuditObj not supplied in argument."; |
409
|
|
|
|
|
|
|
|
410
|
0
|
|
0
|
|
|
0
|
my $Auditors = $Row->result_source->schema->auditors || []; |
411
|
0
|
|
|
|
|
0
|
my $found = 0; |
412
|
0
|
|
0
|
|
|
0
|
$_ == $AuditObj and $found = 1 for (@$Auditors); |
413
|
0
|
0
|
|
|
|
0
|
die "Supplied AuditObj is not an active Auditor on this Row's schema instance" |
414
|
|
|
|
|
|
|
unless ($found); |
415
|
|
|
|
|
|
|
|
416
|
0
|
|
|
|
|
0
|
my $source_name = $Row->result_source->source_name; |
417
|
0
|
0
|
|
|
|
0
|
my $SourceContext = $AuditObj->tracked_sources->{$source_name} |
418
|
|
|
|
|
|
|
or die "Source '$source_name' is not being tracked by the supplied Auditor"; |
419
|
|
|
|
|
|
|
|
420
|
0
|
0
|
|
|
|
0
|
unless ($AuditObj->active_changeset) { |
421
|
0
|
|
|
|
|
0
|
$AuditObj->start_changeset; |
422
|
0
|
|
|
|
|
0
|
$AuditObj->auto_finish(1); |
423
|
|
|
|
|
|
|
} |
424
|
|
|
|
|
|
|
|
425
|
0
|
|
|
|
|
0
|
my $class = $AuditObj->change_context_class; |
426
|
0
|
|
|
|
|
0
|
my $ChangeContext = $class->new( |
427
|
|
|
|
|
|
|
AuditObj => $AuditObj, |
428
|
|
|
|
|
|
|
SourceContext => $SourceContext, |
429
|
|
|
|
|
|
|
ChangeSetContext => $AuditObj->active_changeset, |
430
|
|
|
|
|
|
|
Row => $Row, |
431
|
|
|
|
|
|
|
new_columns => { $Row->get_columns }, |
432
|
|
|
|
|
|
|
action => 'select' |
433
|
|
|
|
|
|
|
); |
434
|
0
|
|
|
|
|
0
|
$ChangeContext->record; |
435
|
0
|
|
|
|
|
0
|
$AuditObj->record_changes($ChangeContext); |
436
|
0
|
|
|
|
|
0
|
return $Row; |
437
|
92
|
|
|
|
|
6004
|
}); |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
$meta->add_method( audit_init => sub { |
440
|
0
|
|
|
0
|
|
0
|
my $Row = shift; |
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
441
|
0
|
0
|
|
|
|
0
|
my $AuditObj = shift or die "AuditObj not supplied in argument."; |
442
|
|
|
|
|
|
|
|
443
|
0
|
|
0
|
|
|
0
|
my $Auditors = $Row->result_source->schema->auditors || []; |
444
|
0
|
|
|
|
|
0
|
my $found = 0; |
445
|
0
|
|
0
|
|
|
0
|
$_ == $AuditObj and $found = 1 for (@$Auditors); |
446
|
0
|
0
|
|
|
|
0
|
die "Supplied AuditObj is not an active Auditor on this Row's schema instance" |
447
|
|
|
|
|
|
|
unless ($found); |
448
|
|
|
|
|
|
|
|
449
|
0
|
|
|
|
|
0
|
my $Collector = $AuditObj->collector; |
450
|
0
|
0
|
|
|
|
0
|
return $Row->audit_take_snapshot($AuditObj) unless ($Collector->has_full_row_stored($Row)); |
451
|
0
|
|
|
|
|
0
|
return $Row; |
452
|
92
|
|
|
|
|
5906
|
}); |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
# Restore immutability to the way to was: |
455
|
92
|
50
|
|
|
|
4153
|
$meta->make_immutable(%immut_opts) if ($immutable); |
456
|
|
|
|
|
|
|
} |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
########## |
460
|
|
|
|
|
|
|
########## |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
# Starts a new changeset if there isn't one active: |
463
|
|
|
|
|
|
|
sub start_unless_changeset { |
464
|
140
|
|
|
140
|
1
|
882
|
my $self = shift; |
465
|
140
|
100
|
|
|
|
2429
|
return $self->active_changeset ? undef : $self->start_changeset; |
466
|
|
|
|
|
|
|
} |
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
sub start_changeset { |
469
|
49
|
|
|
49
|
1
|
611
|
my $self = shift; |
470
|
49
|
50
|
|
|
|
861
|
die "Cannot start_changeset because a changeset is already active" if ($self->active_changeset); |
471
|
|
|
|
|
|
|
|
472
|
49
|
|
|
|
|
1365
|
my $class = $self->changeset_context_class; |
473
|
49
|
|
|
|
|
1217
|
$self->active_changeset($class->new( AuditObj => $self )); |
474
|
49
|
|
|
|
|
72366
|
return $self->active_changeset; |
475
|
|
|
|
|
|
|
} |
476
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
sub finish_if_changeset { |
478
|
28
|
|
|
28
|
1
|
431
|
my $self = shift; |
479
|
28
|
100
|
|
|
|
491
|
return $self->active_changeset ? $self->finish_changeset : undef; |
480
|
|
|
|
|
|
|
} |
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
has '_finishing_changeset', is => 'rw', isa => Bool, default => sub{0}; |
483
|
|
|
|
|
|
|
sub finish_changeset { |
484
|
50
|
|
|
50
|
1
|
344
|
my $self = shift; |
485
|
50
|
50
|
|
|
|
843
|
die "Cannot finish_changeset because there isn't one active" unless ($self->active_changeset); |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
# Protect against deep recursion. This is needed for cases where the collector |
488
|
|
|
|
|
|
|
# is writing to tables within the tracked schema |
489
|
50
|
100
|
|
|
|
1225
|
return if ($self->_finishing_changeset); |
490
|
49
|
|
|
|
|
1237
|
$self->_finishing_changeset(1); |
491
|
|
|
|
|
|
|
|
492
|
49
|
50
|
|
|
|
2468
|
unless($self->record_empty_changes) { |
493
|
49
|
|
|
|
|
128
|
my $count_cols = 0; |
494
|
|
|
|
|
|
|
$count_cols = $count_cols + scalar($_->all_column_changes) |
495
|
49
|
|
|
|
|
116
|
for (@{$self->active_changeset->changes}); |
|
49
|
|
|
|
|
820
|
|
496
|
49
|
100
|
|
|
|
4060
|
unless ($count_cols > 0) { |
497
|
2
|
|
|
|
|
7
|
$self->clear_changeset; |
498
|
2
|
|
|
|
|
158
|
return 1; |
499
|
|
|
|
|
|
|
} |
500
|
|
|
|
|
|
|
} |
501
|
|
|
|
|
|
|
|
502
|
47
|
|
|
|
|
1321
|
$self->active_changeset->finish; |
503
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
##### |
505
|
47
|
|
|
|
|
3246
|
$self->collector->record_changes($self->active_changeset); |
506
|
|
|
|
|
|
|
##### |
507
|
|
|
|
|
|
|
|
508
|
47
|
|
|
|
|
2044875
|
$self->clear_changeset; |
509
|
47
|
|
|
|
|
2840
|
return 1; |
510
|
|
|
|
|
|
|
} |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
sub _exception_cleanup { |
513
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
514
|
0
|
|
|
|
|
0
|
my $err = shift; |
515
|
0
|
|
|
|
|
0
|
$self->clear_changeset; |
516
|
0
|
|
|
|
|
0
|
$self->_current_change_group([]); |
517
|
|
|
|
|
|
|
} |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
sub clear_changeset { |
520
|
49
|
|
|
49
|
1
|
6657
|
my $self = shift; |
521
|
49
|
|
|
|
|
1837
|
$self->active_changeset(undef); |
522
|
49
|
|
|
|
|
5176
|
$self->auto_finish(0); |
523
|
49
|
|
|
|
|
3408
|
$self->_finishing_changeset(0); |
524
|
|
|
|
|
|
|
} |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
sub record_changes { |
527
|
93
|
|
|
93
|
1
|
1094
|
my ($self, @ChangeContexts) = @_; |
528
|
|
|
|
|
|
|
|
529
|
93
|
|
|
|
|
311
|
my $local_changeset = $self->start_unless_changeset; |
530
|
|
|
|
|
|
|
|
531
|
93
|
|
|
|
|
2051
|
$self->active_changeset->add_changes($_) for (@ChangeContexts); |
532
|
|
|
|
|
|
|
|
533
|
93
|
100
|
|
|
|
396
|
$self->finish_changeset if ($local_changeset); |
534
|
|
|
|
|
|
|
} |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
## Change 'group' vs Change 'set' |
538
|
|
|
|
|
|
|
# |
539
|
|
|
|
|
|
|
# I am using the term 'group' (to distinguish from 'set') to represent a group |
540
|
|
|
|
|
|
|
# of changes (rows) that are being changed within a single query/sql statement. |
541
|
|
|
|
|
|
|
# (vs. set which is any number of query/sql statements grouped in a transaction) |
542
|
|
|
|
|
|
|
# This should only happen from making changes via ResultSet objects instead of |
543
|
|
|
|
|
|
|
# Row objects, and in these cases we normalize these into individual (row) changes |
544
|
|
|
|
|
|
|
# TODO: should ChangeGroup be made into a 6th Context? For now, I think no because |
545
|
|
|
|
|
|
|
# it is overkill. |
546
|
|
|
|
|
|
|
## |
547
|
|
|
|
|
|
|
|
548
|
|
|
|
|
|
|
# -- This is a glorified tmp variable used just to allow groups of changes |
549
|
|
|
|
|
|
|
# to be associated with the correct auditor. TODO: This is probably a |
550
|
|
|
|
|
|
|
# poor solution to a complex scoping problem. This exposes us to the |
551
|
|
|
|
|
|
|
# risk of processing stale data, so we have to be sure (manually) to keep |
552
|
|
|
|
|
|
|
# this clear/empty outside its *very* short lifespan, by regularly resetting it |
553
|
|
|
|
|
|
|
has '_current_change_group', is => 'rw', isa => ArrayRef[Object], default => sub{[]}; |
554
|
|
|
|
|
|
|
# -- |
555
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
sub _start_current_change_group { |
557
|
100
|
|
|
100
|
|
380
|
my ($self, $Source, $nested, $action, @changes) = @_; |
558
|
|
|
|
|
|
|
|
559
|
100
|
|
50
|
|
|
1994
|
my $Group = $self->_current_change_group || []; |
560
|
100
|
100
|
|
|
|
1105
|
$Group = [] unless ($nested); |
561
|
|
|
|
|
|
|
|
562
|
100
|
|
|
|
|
1738
|
$self->_current_change_group($Group); # just for good measure |
563
|
|
|
|
|
|
|
|
564
|
100
|
|
|
|
|
7159
|
my $source_name = $Source->source_name; |
565
|
100
|
|
|
|
|
322
|
my $func_name = $source_name . '::' . $action; |
566
|
|
|
|
|
|
|
|
567
|
100
|
100
|
|
|
|
577
|
return () unless ($self->tracked_action_functions->{$func_name}); |
568
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
my @ChangeContexts = map { |
570
|
87
|
|
|
|
|
250
|
$self->_new_change_context( |
571
|
|
|
|
|
|
|
AuditObj => $self, |
572
|
98
|
|
|
|
|
2316
|
SourceContext => $self->tracked_sources->{$source_name}, |
573
|
|
|
|
|
|
|
ChangeSetContext => $self->active_changeset, # could be undef |
574
|
|
|
|
|
|
|
action => $action, |
575
|
|
|
|
|
|
|
$self->_validated_change_hash($_) |
576
|
|
|
|
|
|
|
) |
577
|
|
|
|
|
|
|
} @changes; |
578
|
|
|
|
|
|
|
|
579
|
87
|
|
|
|
|
3525
|
push @$Group, @ChangeContexts; |
580
|
87
|
|
|
|
|
1603
|
$self->_current_change_group($Group); |
581
|
87
|
|
|
|
|
6441
|
return @ChangeContexts; |
582
|
|
|
|
|
|
|
} |
583
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
sub _validated_change_hash { |
585
|
98
|
|
|
98
|
|
926
|
my ($self, $data) = @_; |
586
|
|
|
|
|
|
|
|
587
|
98
|
|
|
|
|
9032
|
require Data::Dumper::Concise; |
588
|
|
|
|
|
|
|
|
589
|
98
|
50
|
|
|
|
5928
|
die "change data must be a HashRef:\n" . |
590
|
|
|
|
|
|
|
Data::Dumper::Concise::Dumper($data) unless (ref($data) eq 'HASH'); |
591
|
|
|
|
|
|
|
|
592
|
98
|
|
|
|
|
255
|
my %allowed_keys = map {$_=>1} qw(old_columns to_columns new_columns); |
|
294
|
|
|
|
|
854
|
|
593
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
$allowed_keys{$_} && ref($data->{$_}) eq 'HASH' or |
595
|
|
|
|
|
|
|
die "Bad data in change hash:\n" . Data::Dumper::Concise::Dumper($data) |
596
|
98
|
|
33
|
|
|
416
|
for (grep { $_ ne 'condition' } keys %$data); |
|
163
|
|
50
|
|
|
1123
|
|
597
|
|
|
|
|
|
|
|
598
|
98
|
|
|
|
|
566
|
return %$data; |
599
|
|
|
|
|
|
|
} |
600
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
sub _finish_current_change_group { |
603
|
93
|
|
|
93
|
|
1362
|
my $self = shift; |
604
|
93
|
50
|
|
|
|
197
|
$self->record_changes(@{$self->_current_change_group || []}); |
|
93
|
|
|
|
|
1646
|
|
605
|
93
|
|
|
|
|
1962
|
$self->_current_change_group([]); #<-- critical to reset! |
606
|
|
|
|
|
|
|
} |
607
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
# factory-like helper: |
609
|
|
|
|
|
|
|
sub _new_change_context { |
610
|
98
|
|
|
98
|
|
249
|
my $self = shift; |
611
|
98
|
|
|
|
|
2353
|
my $class = $self->change_context_class; |
612
|
98
|
|
|
|
|
2364
|
return $class->new(@_); |
613
|
|
|
|
|
|
|
} |
614
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
1; |
618
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
__END__ |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
=head1 NAME |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
DBIx::Class::AuditAny - Flexible change tracking framework for L<DBIx::Class> |
625
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
=begin HTML |
627
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
<a href='https://coveralls.io/r/vanstyn/DBIx-Class-AuditAny?branch=master'> |
629
|
|
|
|
|
|
|
<img |
630
|
|
|
|
|
|
|
src='https://coveralls.io/repos/vanstyn/DBIx-Class-AuditAny/badge.svg?branch=master' |
631
|
|
|
|
|
|
|
alt='Coverage Status' |
632
|
|
|
|
|
|
|
/> |
633
|
|
|
|
|
|
|
</a> |
634
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
=end HTML |
636
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
=head1 SYNOPSIS |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
my $schema = My::Schema->connect(@connect); |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
use DBIx::Class::AuditAny; |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
my $Auditor = DBIx::Class::AuditAny->track( |
644
|
|
|
|
|
|
|
schema => $schema, |
645
|
|
|
|
|
|
|
track_all_sources => 1, |
646
|
|
|
|
|
|
|
collector_class => 'Collector::AutoDBIC', |
647
|
|
|
|
|
|
|
collector_params => { |
648
|
|
|
|
|
|
|
sqlite_db => 'db/audit.db', |
649
|
|
|
|
|
|
|
} |
650
|
|
|
|
|
|
|
); |
651
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
=head1 DESCRIPTION |
653
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
This module provides a generalized way to track changes to DBIC databases. The aim is |
655
|
|
|
|
|
|
|
to provide quick/turn-key options to be able to hit the ground running, while also |
656
|
|
|
|
|
|
|
being highly flexible and customizable with sane APIs. |
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
C<DBIx::Class::AuditAny> wants to be a general framework on top of which other Change |
659
|
|
|
|
|
|
|
Tracking modules for DBIC can be written, while also providing fully fleshed, end-user |
660
|
|
|
|
|
|
|
solutions that can be dropped in and work out-of-the-box. |
661
|
|
|
|
|
|
|
|
662
|
|
|
|
|
|
|
=head2 Background |
663
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
This module was originally written in 2012 for an internal client project, and the process |
665
|
|
|
|
|
|
|
of getting it released open-source as a stand-alone, general-purpose module was started in |
666
|
|
|
|
|
|
|
2013. However, I got busy with other projects and wasn't able to complete a CPAN release at |
667
|
|
|
|
|
|
|
that time (mainly due to missing docs and minor loose ends). I finally came back to this |
668
|
|
|
|
|
|
|
project (May 2015) to actually get a release out to CPAN. So, even though the release date |
669
|
|
|
|
|
|
|
is in 2015, the majority of the code is actually several years old (and has been running |
670
|
|
|
|
|
|
|
perfectly in production for several client apps the whole time). |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
=head2 API and Usage |
674
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
AuditAny uses a different API than typical DBIC components. Instead of loading at the |
676
|
|
|
|
|
|
|
schema/result class level with C<load_components>, AuditAny is used by attaching an |
677
|
|
|
|
|
|
|
"Auditor" to an existing schema I<object> instance: |
678
|
|
|
|
|
|
|
|
679
|
|
|
|
|
|
|
my $schema = My::Schema->connect(@connect); |
680
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
my $Auditor = DBIx::Class::AuditAny->track( |
682
|
|
|
|
|
|
|
schema => $schema, |
683
|
|
|
|
|
|
|
track_all_sources => 1, |
684
|
|
|
|
|
|
|
collector_class => 'Collector::AutoDBIC', |
685
|
|
|
|
|
|
|
collector_params => { |
686
|
|
|
|
|
|
|
sqlite_db => 'db/audit.db', |
687
|
|
|
|
|
|
|
} |
688
|
|
|
|
|
|
|
); |
689
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
The rationale of this approach is that change tracking isn't necessarily something that |
691
|
|
|
|
|
|
|
needs to be, or should be, defined as a built-in attribute of the schema class. |
692
|
|
|
|
|
|
|
Additionally, because of the object-based approach, it is possible to attach multiple |
693
|
|
|
|
|
|
|
Auditors to a single schema object with multiple calls to DBIx::Class::AuditAny->track. |
694
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
=head1 DATAPOINTS |
696
|
|
|
|
|
|
|
|
697
|
|
|
|
|
|
|
As changes occur in the tracked schema, information is collected in the form of |
698
|
|
|
|
|
|
|
I<datapoints> at various stages - or I<contexts> - before being passed to the |
699
|
|
|
|
|
|
|
configured Collector. A datapoint has a globally unique name and code used to calculate |
700
|
|
|
|
|
|
|
its value. Code is called at the stage defined by the I<context> of the datapoint. |
701
|
|
|
|
|
|
|
The available contexts are: |
702
|
|
|
|
|
|
|
|
703
|
|
|
|
|
|
|
=over 4 |
704
|
|
|
|
|
|
|
|
705
|
|
|
|
|
|
|
=item set |
706
|
|
|
|
|
|
|
|
707
|
|
|
|
|
|
|
=over 5 |
708
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
=item base |
710
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
=back |
712
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
=item change |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
=over 5 |
716
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
=item source |
718
|
|
|
|
|
|
|
|
719
|
|
|
|
|
|
|
=back |
720
|
|
|
|
|
|
|
|
721
|
|
|
|
|
|
|
=item column |
722
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
=back |
725
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
B<set> (AKA changeset) datapoints are specific to an entire set of changes - insert/ |
727
|
|
|
|
|
|
|
update/delete statements grouped in a transaction. Example changeset datapoints include |
728
|
|
|
|
|
|
|
C<changeset_ts> and other broad items. B<base> datapoints are logically the same as |
729
|
|
|
|
|
|
|
B<set> but only need to be calculated once (instead of with every change set). These |
730
|
|
|
|
|
|
|
include things like C<schema> and C<schema_ver>. |
731
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
B<change> datapoints apply to a specific C<insert>, C<update> or C<delete> statement, |
733
|
|
|
|
|
|
|
and range from simple items such as C<action> (one of 'insert', 'update' or 'delete') |
734
|
|
|
|
|
|
|
to more exotic and complex items like C<column_changes_json>. B<source> datapoints are |
735
|
|
|
|
|
|
|
logically the same as B<change>, but like B<base> datapoints, only need to be |
736
|
|
|
|
|
|
|
calculated once (per source). These include things like C<table_name> and C<source> |
737
|
|
|
|
|
|
|
(source name). |
738
|
|
|
|
|
|
|
|
739
|
|
|
|
|
|
|
Finally, B<column> datapoints cover information specific to an individual column, such |
740
|
|
|
|
|
|
|
as C<column_name>, C<old_value> and C<new_value>. |
741
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
There are a number of built-in datapoints (currently stored in |
743
|
|
|
|
|
|
|
L<DBIx::Class::AuditAny::Util::BuiltinDatapoints> which is likely to change), but custom |
744
|
|
|
|
|
|
|
datapoints can also be defined. The Auditor config defines a specific set of datapoints to |
745
|
|
|
|
|
|
|
be calculated (built-in and/or custom). If no datapoints are specified, the default list is used |
746
|
|
|
|
|
|
|
(currently C<change_ts, action, source, pri_key_value, column_name, old_value, new_value>). |
747
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
The list of datapoints is specified as an ArrayRef in the config. For example: |
749
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
datapoints => [qw(action_id column_name new_value)], |
751
|
|
|
|
|
|
|
|
752
|
|
|
|
|
|
|
=head2 Custom Datapoints |
753
|
|
|
|
|
|
|
|
754
|
|
|
|
|
|
|
Custom datapoints are specified as HashRef configs with 3 parameters: |
755
|
|
|
|
|
|
|
|
756
|
|
|
|
|
|
|
=over 4 |
757
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
=item name |
759
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
The unique name of the datapoint. Should be all lowercase letters, numbers and |
761
|
|
|
|
|
|
|
underscore and must be different from all other datapoints (across all contexts). |
762
|
|
|
|
|
|
|
|
763
|
|
|
|
|
|
|
=item context |
764
|
|
|
|
|
|
|
|
765
|
|
|
|
|
|
|
The context of the datapoint: base, source, set, change or column. |
766
|
|
|
|
|
|
|
|
767
|
|
|
|
|
|
|
=item method |
768
|
|
|
|
|
|
|
|
769
|
|
|
|
|
|
|
CodeRef to calculate and return the value. The CodeRef is called according to the |
770
|
|
|
|
|
|
|
context, and a different context object is supplied for each context. Each context has |
771
|
|
|
|
|
|
|
its own context object type except B<base> which is supplied the Auditor object itself. |
772
|
|
|
|
|
|
|
See Audit Context Objects below. |
773
|
|
|
|
|
|
|
|
774
|
|
|
|
|
|
|
=back |
775
|
|
|
|
|
|
|
|
776
|
|
|
|
|
|
|
|
777
|
|
|
|
|
|
|
Custom datapoints are defined in the C<datapoint_configs> param. After defining a new |
778
|
|
|
|
|
|
|
datapoint config it can then be used like any other datapoint. For example: |
779
|
|
|
|
|
|
|
|
780
|
|
|
|
|
|
|
datapoints => [qw(action_id column_name new_value client_ip)], |
781
|
|
|
|
|
|
|
datapoint_configs => [ |
782
|
|
|
|
|
|
|
{ |
783
|
|
|
|
|
|
|
name => 'client_ip', |
784
|
|
|
|
|
|
|
context => 'set', |
785
|
|
|
|
|
|
|
method => sub { |
786
|
|
|
|
|
|
|
my $contextObj = shift; |
787
|
|
|
|
|
|
|
my $c = some_func(...); |
788
|
|
|
|
|
|
|
return $c->req->address; |
789
|
|
|
|
|
|
|
} |
790
|
|
|
|
|
|
|
} |
791
|
|
|
|
|
|
|
] |
792
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
=head2 Datapoint Names |
794
|
|
|
|
|
|
|
|
795
|
|
|
|
|
|
|
Datapoint names must be unique, which means all the built-in datapoint names are |
796
|
|
|
|
|
|
|
reserved. However, if you really want to use an existing datapoint name, or if you want |
797
|
|
|
|
|
|
|
a built-in datapoint to use a different name, you can rename any datapoints like so: |
798
|
|
|
|
|
|
|
|
799
|
|
|
|
|
|
|
rename_datapoints => { |
800
|
|
|
|
|
|
|
new_value => 'new', |
801
|
|
|
|
|
|
|
old_value => 'old', |
802
|
|
|
|
|
|
|
column_name => 'column', |
803
|
|
|
|
|
|
|
}, |
804
|
|
|
|
|
|
|
|
805
|
|
|
|
|
|
|
=head1 COLLECTORS |
806
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
Once the Auditor calculates the configured datapoints it passes them to the configured |
808
|
|
|
|
|
|
|
I<Collector>. There are several built-in Collectors provided, but writing a custom Collector |
809
|
|
|
|
|
|
|
is a trivial matter. All you need to do is write a L<Moo>-compatible class which consumes |
810
|
|
|
|
|
|
|
the L<DBIx::Class::AuditAny::Role::Collector> role and implement a C<record_changes()> method. |
811
|
|
|
|
|
|
|
This method is called with a L<ChangeSet|DBIx::Class::AuditAny::AuditContext::ChangeSet> object |
812
|
|
|
|
|
|
|
supplied as the argument at the end of every database transaction which performs a write operation. |
813
|
|
|
|
|
|
|
|
814
|
|
|
|
|
|
|
No matter how small or large the transaction, the ChangeSet object provides APIs to a nested |
815
|
|
|
|
|
|
|
structure to be able to access all information regarding what changed during the given transaction. |
816
|
|
|
|
|
|
|
(See L<AUDIT CONTEXT OBJECTS|DBIx::Class::AuditAny#AUDIT_CONTEXT_OBJECTS> below). |
817
|
|
|
|
|
|
|
|
818
|
|
|
|
|
|
|
|
819
|
|
|
|
|
|
|
=head2 Supplied Collector Classes |
820
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
The following built-in collector classes are already provided: |
822
|
|
|
|
|
|
|
|
823
|
|
|
|
|
|
|
=over |
824
|
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
=item * |
826
|
|
|
|
|
|
|
|
827
|
|
|
|
|
|
|
L<DBIx::Class::AuditAny::Collector::AutoDBIC> |
828
|
|
|
|
|
|
|
|
829
|
|
|
|
|
|
|
=item * |
830
|
|
|
|
|
|
|
|
831
|
|
|
|
|
|
|
L<DBIx::Class::AuditAny::Collector::DBIC> |
832
|
|
|
|
|
|
|
|
833
|
|
|
|
|
|
|
=item * |
834
|
|
|
|
|
|
|
|
835
|
|
|
|
|
|
|
L<DBIx::Class::AuditAny::Collector::Code> |
836
|
|
|
|
|
|
|
|
837
|
|
|
|
|
|
|
=back |
838
|
|
|
|
|
|
|
|
839
|
|
|
|
|
|
|
=head1 AUDIT CONTEXT OBJECTS |
840
|
|
|
|
|
|
|
|
841
|
|
|
|
|
|
|
Inspired in part by the Catalyst Context object design, the internal machinery which captures and |
842
|
|
|
|
|
|
|
organizes the change datapoints associated with a modifying transaction is wrapped in a nested |
843
|
|
|
|
|
|
|
structure of 3 kinds of "context" objects: |
844
|
|
|
|
|
|
|
|
845
|
|
|
|
|
|
|
=over |
846
|
|
|
|
|
|
|
|
847
|
|
|
|
|
|
|
=item * |
848
|
|
|
|
|
|
|
|
849
|
|
|
|
|
|
|
L<DBIx::Class::AuditAny::AuditContext::ChangeSet> |
850
|
|
|
|
|
|
|
|
851
|
|
|
|
|
|
|
=item * |
852
|
|
|
|
|
|
|
|
853
|
|
|
|
|
|
|
L<DBIx::Class::AuditAny::AuditContext::Change> |
854
|
|
|
|
|
|
|
|
855
|
|
|
|
|
|
|
=item * |
856
|
|
|
|
|
|
|
|
857
|
|
|
|
|
|
|
L<DBIx::Class::AuditAny::AuditContext::Column> |
858
|
|
|
|
|
|
|
|
859
|
|
|
|
|
|
|
=back |
860
|
|
|
|
|
|
|
|
861
|
|
|
|
|
|
|
This provides a clean and straightforward API for which Collector classes are able to identify and |
862
|
|
|
|
|
|
|
act on the data in any manner they want, be it recording to a database, logging to a simple file, |
863
|
|
|
|
|
|
|
or taking any kind of programmatic action. Collectors can really be thought of as a structure for |
864
|
|
|
|
|
|
|
powerful external triggers. |
865
|
|
|
|
|
|
|
|
866
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
867
|
|
|
|
|
|
|
|
868
|
|
|
|
|
|
|
Note: Documentation of all the individual attrs and methods of this class (shown below) is still |
869
|
|
|
|
|
|
|
TBD. However, most meaningful scenarios involving interacting with these is already covered above, |
870
|
|
|
|
|
|
|
or is covered further down in the L<Examples|DBIx::Class::AuditAny#EXAMPLES>. |
871
|
|
|
|
|
|
|
|
872
|
|
|
|
|
|
|
=head2 datapoints |
873
|
|
|
|
|
|
|
|
874
|
|
|
|
|
|
|
=head2 allow_multiple_auditors |
875
|
|
|
|
|
|
|
|
876
|
|
|
|
|
|
|
=head2 auto_include_user_defined_datapoints |
877
|
|
|
|
|
|
|
|
878
|
|
|
|
|
|
|
=head2 build_init_args |
879
|
|
|
|
|
|
|
|
880
|
|
|
|
|
|
|
=head2 calling_action_function |
881
|
|
|
|
|
|
|
|
882
|
|
|
|
|
|
|
=head2 change_context_class |
883
|
|
|
|
|
|
|
|
884
|
|
|
|
|
|
|
=head2 changeset_context_class |
885
|
|
|
|
|
|
|
|
886
|
|
|
|
|
|
|
=head2 collector_class |
887
|
|
|
|
|
|
|
|
888
|
|
|
|
|
|
|
=head2 collector_params |
889
|
|
|
|
|
|
|
|
890
|
|
|
|
|
|
|
=head2 column_context_class |
891
|
|
|
|
|
|
|
|
892
|
|
|
|
|
|
|
=head2 datapoint_configs |
893
|
|
|
|
|
|
|
|
894
|
|
|
|
|
|
|
=head2 default_datapoint_class |
895
|
|
|
|
|
|
|
|
896
|
|
|
|
|
|
|
=head2 disable_datapoints |
897
|
|
|
|
|
|
|
|
898
|
|
|
|
|
|
|
=head2 primary_key_separator |
899
|
|
|
|
|
|
|
|
900
|
|
|
|
|
|
|
=head2 record_empty_changes |
901
|
|
|
|
|
|
|
|
902
|
|
|
|
|
|
|
=head2 rename_datapoints |
903
|
|
|
|
|
|
|
|
904
|
|
|
|
|
|
|
=head2 schema |
905
|
|
|
|
|
|
|
|
906
|
|
|
|
|
|
|
=head2 source_context_class |
907
|
|
|
|
|
|
|
|
908
|
|
|
|
|
|
|
=head2 time_zone |
909
|
|
|
|
|
|
|
|
910
|
|
|
|
|
|
|
=head2 track_actions |
911
|
|
|
|
|
|
|
|
912
|
|
|
|
|
|
|
=head2 track_immutable |
913
|
|
|
|
|
|
|
|
914
|
|
|
|
|
|
|
=head2 track_init_args |
915
|
|
|
|
|
|
|
|
916
|
|
|
|
|
|
|
=head2 tracked_action_functions |
917
|
|
|
|
|
|
|
|
918
|
|
|
|
|
|
|
=head2 tracked_sources |
919
|
|
|
|
|
|
|
|
920
|
|
|
|
|
|
|
=head1 METHODS |
921
|
|
|
|
|
|
|
|
922
|
|
|
|
|
|
|
=head2 get_dt |
923
|
|
|
|
|
|
|
|
924
|
|
|
|
|
|
|
=head2 track |
925
|
|
|
|
|
|
|
|
926
|
|
|
|
|
|
|
=head2 get_datapoint_orig |
927
|
|
|
|
|
|
|
|
928
|
|
|
|
|
|
|
=head2 add_datapoints |
929
|
|
|
|
|
|
|
|
930
|
|
|
|
|
|
|
=head2 all_datapoints |
931
|
|
|
|
|
|
|
|
932
|
|
|
|
|
|
|
=head2 get_context_datapoint_names |
933
|
|
|
|
|
|
|
|
934
|
|
|
|
|
|
|
=head2 get_context_datapoints |
935
|
|
|
|
|
|
|
|
936
|
|
|
|
|
|
|
=head2 local_datapoint_data |
937
|
|
|
|
|
|
|
|
938
|
|
|
|
|
|
|
=head2 track_sources |
939
|
|
|
|
|
|
|
|
940
|
|
|
|
|
|
|
=head2 track_all_sources |
941
|
|
|
|
|
|
|
|
942
|
|
|
|
|
|
|
=head2 init_all_sources |
943
|
|
|
|
|
|
|
|
944
|
|
|
|
|
|
|
Calls C<init_sources> with all tracked source names |
945
|
|
|
|
|
|
|
|
946
|
|
|
|
|
|
|
=head2 init_sources |
947
|
|
|
|
|
|
|
|
948
|
|
|
|
|
|
|
Special-purpose method to initialize rows for the case of starting auditing a database with |
949
|
|
|
|
|
|
|
existing data. This will simulate changes with the special C<'select'> action. This is useful |
950
|
|
|
|
|
|
|
to be able to use the audit database to follow changes backward to a starting point, and having |
951
|
|
|
|
|
|
|
that state fully recorded, just as if auditing had been enabled when the rows were inserted. |
952
|
|
|
|
|
|
|
|
953
|
|
|
|
|
|
|
This method accepts a list of source names and makes sure that every row of each source is |
954
|
|
|
|
|
|
|
initialized. So, be careful, as this can be a very heavy operation depending on the number |
955
|
|
|
|
|
|
|
of rows. This is a tool that would generally only be used interactively during a new setup. |
956
|
|
|
|
|
|
|
|
957
|
|
|
|
|
|
|
=head2 start_unless_changeset |
958
|
|
|
|
|
|
|
|
959
|
|
|
|
|
|
|
=head2 start_changeset |
960
|
|
|
|
|
|
|
|
961
|
|
|
|
|
|
|
=head2 finish_changeset |
962
|
|
|
|
|
|
|
|
963
|
|
|
|
|
|
|
=head2 finish_if_changeset |
964
|
|
|
|
|
|
|
|
965
|
|
|
|
|
|
|
=head2 clear_changeset |
966
|
|
|
|
|
|
|
|
967
|
|
|
|
|
|
|
=head2 record_changes |
968
|
|
|
|
|
|
|
|
969
|
|
|
|
|
|
|
|
970
|
|
|
|
|
|
|
=head1 EXAMPLES |
971
|
|
|
|
|
|
|
|
972
|
|
|
|
|
|
|
=head3 simple dedicated audit db |
973
|
|
|
|
|
|
|
|
974
|
|
|
|
|
|
|
Record all changes into a *separate*, auto-generated and initialized SQLite schema/db |
975
|
|
|
|
|
|
|
with default datapoints (Quickest/simplest usage - SYNOPSIS example): |
976
|
|
|
|
|
|
|
|
977
|
|
|
|
|
|
|
Uses the Collector L<DBIx::Class::AuditAny::Collector::AutoDBIC> |
978
|
|
|
|
|
|
|
|
979
|
|
|
|
|
|
|
my $schema = My::Schema->connect(@connect); |
980
|
|
|
|
|
|
|
|
981
|
|
|
|
|
|
|
use DBIx::Class::AuditAny; |
982
|
|
|
|
|
|
|
|
983
|
|
|
|
|
|
|
my $Auditor = DBIx::Class::AuditAny->track( |
984
|
|
|
|
|
|
|
schema => $schema, |
985
|
|
|
|
|
|
|
track_all_sources => 1, |
986
|
|
|
|
|
|
|
collector_class => 'Collector::AutoDBIC', |
987
|
|
|
|
|
|
|
collector_params => { |
988
|
|
|
|
|
|
|
sqlite_db => 'db/audit.db', |
989
|
|
|
|
|
|
|
} |
990
|
|
|
|
|
|
|
); |
991
|
|
|
|
|
|
|
|
992
|
|
|
|
|
|
|
=head3 recording to the same db |
993
|
|
|
|
|
|
|
|
994
|
|
|
|
|
|
|
Record all changes - into specified target sources within the *same*/tracked |
995
|
|
|
|
|
|
|
schema - using specific datapoints: |
996
|
|
|
|
|
|
|
|
997
|
|
|
|
|
|
|
Uses the Collector L<DBIx::Class::AuditAny::Collector::DBIC> |
998
|
|
|
|
|
|
|
|
999
|
|
|
|
|
|
|
DBIx::Class::AuditAny->track( |
1000
|
|
|
|
|
|
|
schema => $schema, |
1001
|
|
|
|
|
|
|
track_all_sources => 1, |
1002
|
|
|
|
|
|
|
collector_class => 'Collector::DBIC', |
1003
|
|
|
|
|
|
|
collector_params => { |
1004
|
|
|
|
|
|
|
target_source => 'MyChangeSet', # ChangeSet source name |
1005
|
|
|
|
|
|
|
change_data_rel => 'changes', # Change source, via rel within ChangeSet |
1006
|
|
|
|
|
|
|
column_data_rel => 'change_columns', # ColumnChange source, via rel within Change |
1007
|
|
|
|
|
|
|
}, |
1008
|
|
|
|
|
|
|
datapoints => [ # predefined/built-in named datapoints: |
1009
|
|
|
|
|
|
|
(qw(changeset_ts changeset_elapsed)), |
1010
|
|
|
|
|
|
|
(qw(change_elapsed action source pri_key_value)), |
1011
|
|
|
|
|
|
|
(qw(column_name old_value new_value)), |
1012
|
|
|
|
|
|
|
], |
1013
|
|
|
|
|
|
|
); |
1014
|
|
|
|
|
|
|
|
1015
|
|
|
|
|
|
|
|
1016
|
|
|
|
|
|
|
=head3 coderef collector to a file |
1017
|
|
|
|
|
|
|
|
1018
|
|
|
|
|
|
|
Dump raw change data for specific sources (Artist and Album) to a file, |
1019
|
|
|
|
|
|
|
ignore immutable flags in the schema/result classes, and allow more than |
1020
|
|
|
|
|
|
|
one DBIx::Class::AuditAny Auditor to be attached to the same schema object: |
1021
|
|
|
|
|
|
|
|
1022
|
|
|
|
|
|
|
Uses 'collect' sugar param to setup a bare-bones CodeRef Collector |
1023
|
|
|
|
|
|
|
(L<DBIx::Class::AuditAny::Role::Collector>) |
1024
|
|
|
|
|
|
|
|
1025
|
|
|
|
|
|
|
my $Auditor = DBIx::Class::AuditAny->track( |
1026
|
|
|
|
|
|
|
schema => $schema, |
1027
|
|
|
|
|
|
|
track_sources => [qw(Artist Album)], |
1028
|
|
|
|
|
|
|
track_immutable => 1, |
1029
|
|
|
|
|
|
|
allow_multiple_auditors => 1, |
1030
|
|
|
|
|
|
|
collect => sub { |
1031
|
|
|
|
|
|
|
my $cntx = shift; # ChangeSet context object |
1032
|
|
|
|
|
|
|
require Data::Dumper; |
1033
|
|
|
|
|
|
|
print $fh Data::Dumper->Dump([$cntx],[qw(changeset)]); |
1034
|
|
|
|
|
|
|
|
1035
|
|
|
|
|
|
|
# Do other custom stuff... |
1036
|
|
|
|
|
|
|
} |
1037
|
|
|
|
|
|
|
); |
1038
|
|
|
|
|
|
|
|
1039
|
|
|
|
|
|
|
=head3 more customizations |
1040
|
|
|
|
|
|
|
|
1041
|
|
|
|
|
|
|
Record all updates (but *not* inserts/deletes) - into specified target sources |
1042
|
|
|
|
|
|
|
within the same/tracked schema - using specific datapoints, including user-defined |
1043
|
|
|
|
|
|
|
datapoints and built-in datapoints with custom names: |
1044
|
|
|
|
|
|
|
|
1045
|
|
|
|
|
|
|
DBIx::Class::AuditAny->track( |
1046
|
|
|
|
|
|
|
schema => CoolCatalystApp->model('Schema')->schema, |
1047
|
|
|
|
|
|
|
track_all_sources => 1, |
1048
|
|
|
|
|
|
|
track_actions => [qw(update)], |
1049
|
|
|
|
|
|
|
collector_class => 'Collector::DBIC', |
1050
|
|
|
|
|
|
|
collector_params => { |
1051
|
|
|
|
|
|
|
target_source => 'MyChangeSet', # ChangeSet source name |
1052
|
|
|
|
|
|
|
change_data_rel => 'changes', # Change source, via rel within ChangeSet |
1053
|
|
|
|
|
|
|
column_data_rel => 'change_columns', # ColumnChange source, via rel within Change |
1054
|
|
|
|
|
|
|
}, |
1055
|
|
|
|
|
|
|
datapoints => [ |
1056
|
|
|
|
|
|
|
(qw(changeset_ts changeset_elapsed)), |
1057
|
|
|
|
|
|
|
(qw(change_elapsed action_id table_name pri_key_value)), |
1058
|
|
|
|
|
|
|
(qw(column_name old_value new_value)), |
1059
|
|
|
|
|
|
|
], |
1060
|
|
|
|
|
|
|
datapoint_configs => [ |
1061
|
|
|
|
|
|
|
{ |
1062
|
|
|
|
|
|
|
name => 'client_ip', |
1063
|
|
|
|
|
|
|
context => 'set', |
1064
|
|
|
|
|
|
|
method => sub { |
1065
|
|
|
|
|
|
|
my $c = some_func(...); |
1066
|
|
|
|
|
|
|
return $c->req->address; |
1067
|
|
|
|
|
|
|
} |
1068
|
|
|
|
|
|
|
}, |
1069
|
|
|
|
|
|
|
{ |
1070
|
|
|
|
|
|
|
name => 'user_id', |
1071
|
|
|
|
|
|
|
context => 'set', |
1072
|
|
|
|
|
|
|
method => sub { |
1073
|
|
|
|
|
|
|
my $c = some_func(...); |
1074
|
|
|
|
|
|
|
$c->user->id; |
1075
|
|
|
|
|
|
|
} |
1076
|
|
|
|
|
|
|
} |
1077
|
|
|
|
|
|
|
], |
1078
|
|
|
|
|
|
|
rename_datapoints => { |
1079
|
|
|
|
|
|
|
changeset_elapsed => 'total_elapsed', |
1080
|
|
|
|
|
|
|
change_elapsed => 'elapsed', |
1081
|
|
|
|
|
|
|
pri_key_value => 'row_key', |
1082
|
|
|
|
|
|
|
new_value => 'new', |
1083
|
|
|
|
|
|
|
old_value => 'old', |
1084
|
|
|
|
|
|
|
column_name => 'column', |
1085
|
|
|
|
|
|
|
}, |
1086
|
|
|
|
|
|
|
); |
1087
|
|
|
|
|
|
|
|
1088
|
|
|
|
|
|
|
|
1089
|
|
|
|
|
|
|
=head3 user-defined collector |
1090
|
|
|
|
|
|
|
|
1091
|
|
|
|
|
|
|
Record all changes into a user-defined custom Collector class - using |
1092
|
|
|
|
|
|
|
default datapoints: |
1093
|
|
|
|
|
|
|
|
1094
|
|
|
|
|
|
|
my $Auditor = DBIx::Class::AuditAny->track( |
1095
|
|
|
|
|
|
|
schema => $schema, |
1096
|
|
|
|
|
|
|
track_all_sources => 1, |
1097
|
|
|
|
|
|
|
collector_class => '+MyApp::MyCollector', |
1098
|
|
|
|
|
|
|
collector_params => { |
1099
|
|
|
|
|
|
|
foo => 'blah', |
1100
|
|
|
|
|
|
|
anything => $val |
1101
|
|
|
|
|
|
|
} |
1102
|
|
|
|
|
|
|
); |
1103
|
|
|
|
|
|
|
|
1104
|
|
|
|
|
|
|
=head3 query the audit db |
1105
|
|
|
|
|
|
|
|
1106
|
|
|
|
|
|
|
Access/query the audit db of Collector::DBIC and Collector::AutoDBIC collectors: |
1107
|
|
|
|
|
|
|
|
1108
|
|
|
|
|
|
|
my $audit_schema = $Auditor->collector->target_schema; |
1109
|
|
|
|
|
|
|
$audit_schema->resultset('AuditChangeSet')->search({...}); |
1110
|
|
|
|
|
|
|
|
1111
|
|
|
|
|
|
|
# Print the ddl that auto-generated and deployed with a Collector::AutoDBIC collector: |
1112
|
|
|
|
|
|
|
print $audit_schema->resultset('DeployInfo')->first->deployed_ddl; |
1113
|
|
|
|
|
|
|
|
1114
|
|
|
|
|
|
|
=head2 more examples |
1115
|
|
|
|
|
|
|
|
1116
|
|
|
|
|
|
|
See the unit tests (which are extensive) for more examples. |
1117
|
|
|
|
|
|
|
|
1118
|
|
|
|
|
|
|
|
1119
|
|
|
|
|
|
|
=head1 TODO |
1120
|
|
|
|
|
|
|
|
1121
|
|
|
|
|
|
|
=over |
1122
|
|
|
|
|
|
|
|
1123
|
|
|
|
|
|
|
=item * |
1124
|
|
|
|
|
|
|
|
1125
|
|
|
|
|
|
|
Enable tracking multi-primary-key sources (code currently disabled) |
1126
|
|
|
|
|
|
|
|
1127
|
|
|
|
|
|
|
=item * |
1128
|
|
|
|
|
|
|
|
1129
|
|
|
|
|
|
|
Write more tests |
1130
|
|
|
|
|
|
|
|
1131
|
|
|
|
|
|
|
=item * |
1132
|
|
|
|
|
|
|
|
1133
|
|
|
|
|
|
|
Write more documentation |
1134
|
|
|
|
|
|
|
|
1135
|
|
|
|
|
|
|
=item * |
1136
|
|
|
|
|
|
|
|
1137
|
|
|
|
|
|
|
Add more built-in datapoints |
1138
|
|
|
|
|
|
|
|
1139
|
|
|
|
|
|
|
=item * |
1140
|
|
|
|
|
|
|
|
1141
|
|
|
|
|
|
|
Expand the Collector API to be able to provide datapoint configs |
1142
|
|
|
|
|
|
|
|
1143
|
|
|
|
|
|
|
=item * |
1144
|
|
|
|
|
|
|
|
1145
|
|
|
|
|
|
|
Separate set/change/column datapoints into 'pre' and 'post' stages |
1146
|
|
|
|
|
|
|
|
1147
|
|
|
|
|
|
|
=item * |
1148
|
|
|
|
|
|
|
|
1149
|
|
|
|
|
|
|
Add mechanism to enable/disable tracking (localizable global?) |
1150
|
|
|
|
|
|
|
|
1151
|
|
|
|
|
|
|
=item * |
1152
|
|
|
|
|
|
|
|
1153
|
|
|
|
|
|
|
Switch to use L<Types::Standard> |
1154
|
|
|
|
|
|
|
|
1155
|
|
|
|
|
|
|
=back |
1156
|
|
|
|
|
|
|
|
1157
|
|
|
|
|
|
|
=head1 SIMILAR MODULES |
1158
|
|
|
|
|
|
|
|
1159
|
|
|
|
|
|
|
=head2 DBIx::Class::Journal |
1160
|
|
|
|
|
|
|
|
1161
|
|
|
|
|
|
|
L<DBIx::Class::Journal> was the first DBIC change tracking module released to CPAN. It works, |
1162
|
|
|
|
|
|
|
but is inflexible and mandates a single mode of operation, which is not ideal in many ways. |
1163
|
|
|
|
|
|
|
|
1164
|
|
|
|
|
|
|
=head2 DBIx::Class::AuditLog |
1165
|
|
|
|
|
|
|
|
1166
|
|
|
|
|
|
|
L<DBIx::Class::AuditLog> takes a more casual approach than L<DBIx::Class::Journal>, which makes |
1167
|
|
|
|
|
|
|
it easier to work with. However, it still forces a narrow and specific manner in which it stores |
1168
|
|
|
|
|
|
|
the change history data which doesn't fit all workflows. |
1169
|
|
|
|
|
|
|
|
1170
|
|
|
|
|
|
|
AuditAny was designed specifically for flexibility. By separating the I<Auditor> - which captures the |
1171
|
|
|
|
|
|
|
change data as it happens - from the I<Collector>, which handles storing the data, all sorts of |
1172
|
|
|
|
|
|
|
different styles and manners of formatting and storing the audit data can be achieved. In fact, |
1173
|
|
|
|
|
|
|
L<DBIx::Class::AuditLog> could be written using AuditAny, and store the data in exactly the same |
1174
|
|
|
|
|
|
|
manner by implementing a custom collector class. |
1175
|
|
|
|
|
|
|
|
1176
|
|
|
|
|
|
|
=head2 DBIx::Class::Shadow |
1177
|
|
|
|
|
|
|
|
1178
|
|
|
|
|
|
|
Shadow is a different animal. It is very sophisticated, and places accuracy above all else, with the |
1179
|
|
|
|
|
|
|
idea of being able to do things such as reliably "revive" the previous state of rows, etc. The |
1180
|
|
|
|
|
|
|
downside of this is that it is also not flexible, in that it handles the entire change life cycle |
1181
|
|
|
|
|
|
|
within its logic. This is different from AuditAny, which is more like a packet capture lib for DBIC |
1182
|
|
|
|
|
|
|
(like tcpdump/libpcap is a packet capture lib for networks). Unlike the others, Shadow could B<not> |
1183
|
|
|
|
|
|
|
be implemented using AuditAny, because the I<way> it captures the change data is specific and |
1184
|
|
|
|
|
|
|
fundamentally different. |
1185
|
|
|
|
|
|
|
|
1186
|
|
|
|
|
|
|
Unfortunately, DBIx::Class::Shadow is unfinished and has never been released to CPAN (as of the time |
1187
|
|
|
|
|
|
|
of this writing, in May 2015). Its current, unfinished status can be seen in GitHub: |
1188
|
|
|
|
|
|
|
|
1189
|
|
|
|
|
|
|
=over |
1190
|
|
|
|
|
|
|
|
1191
|
|
|
|
|
|
|
=item * |
1192
|
|
|
|
|
|
|
|
1193
|
|
|
|
|
|
|
L<https://github.com/ribasushi/preshadow> |
1194
|
|
|
|
|
|
|
|
1195
|
|
|
|
|
|
|
=back |
1196
|
|
|
|
|
|
|
|
1197
|
|
|
|
|
|
|
|
1198
|
|
|
|
|
|
|
=head1 SUPPORT |
1199
|
|
|
|
|
|
|
|
1200
|
|
|
|
|
|
|
IRC: |
1201
|
|
|
|
|
|
|
|
1202
|
|
|
|
|
|
|
Join #rapidapp on irc.perl.org. |
1203
|
|
|
|
|
|
|
|
1204
|
|
|
|
|
|
|
=head1 AUTHOR |
1205
|
|
|
|
|
|
|
|
1206
|
|
|
|
|
|
|
Henry Van Styn <vanstyn@cpan.org> |
1207
|
|
|
|
|
|
|
|
1208
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
1209
|
|
|
|
|
|
|
|
1210
|
|
|
|
|
|
|
This software is copyright (c) 2012-2016 by IntelliTree Solutions llc. |
1211
|
|
|
|
|
|
|
|
1212
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
1213
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
1214
|
|
|
|
|
|
|
|
1215
|
|
|
|
|
|
|
=cut |
1216
|
|
|
|
|
|
|
|