log for recreated entries
[dbsrgits/DBIx-Class-Journal.git] / lib / DBIx / Class / Journal.pm
1 package DBIx::Class::Journal;
2
3 use base qw/DBIx::Class/;
4
5 use strict;
6 use warnings;
7
8 our $VERSION = '0.02_01';
9
10 ## On create/insert, add new entry to AuditLog
11
12 # sub new
13 # {
14 #     my ($class, $attrs, @rest) = @_;
15
16 #     $class->result_source->schema->_journal_schema->current_user(delete $attrs->{user_id});
17
18 #     $class->next::method($attrs, @rest);
19 # }
20
21 sub insert
22 {
23     my ($self) = @_;
24
25     return if($self->in_storage);
26     ## create new transaction here?
27     my $res = $self->next::method();
28     if($self->in_storage)
29     {
30         my $s_name = $self->result_source->source_name();
31         my $al = $self->result_source->schema->_journal_schema->resultset("${s_name}AuditLog");
32
33         my %id = map { $_ => $self->get_column($_)} $self->primary_columns;
34
35         my $change = { changeset_id => $al->result_source->schema->current_changeset };
36
37         if ( my $log = $al->find(\%id) ) {
38             #$log->created($change); # FIXME should this work?
39             $log->created($al->related_resultset("created")->create($change));
40             $log->update;
41         } else {
42             $al->create({ %id, created => $change });
43         }
44     }
45
46     return $res;
47 }
48
49 ## On delete, update delete_id of AuditLog
50
51 sub delete
52 {
53     my ($self, @rest) = @_;
54     $self->next::method(@rest);
55
56     if(!$self->in_storage)
57     {
58         my $s_name = $self->result_source->source_name();
59         my $al = $self->result_source->schema->_journal_schema->resultset("${s_name}AuditLog");
60         my $alentry = $al->find_or_create({ map { $_ => $self->get_column($_)} $self->primary_columns });
61          
62         ## bulk_update doesnt do "create new item on update of rel-accessor with hashref, yet
63         my $change = $self->result_source->schema->_journal_schema->resultset('ChangeLog')->create({ changeset_id => $self->result_source->schema->_journal_schema->current_changeset });
64         $alentry->delete_id($change->id);
65         $alentry->update();
66     }
67     
68 }
69
70 ## On update, copy previous row's contents to AuditHistory
71
72 sub update 
73 {
74     my ($self, $upd, @rest) = @_;
75
76     if($self->in_storage)
77     {
78         my $s_name = $self->result_source->source_name();
79         my $ah = $self->result_source->schema->_journal_schema->resultset("${s_name}AuditHistory");
80
81         my $obj = $self->result_source->resultset->find( $self->ident_condition );
82         $ah->create({
83             $obj->get_columns,
84             change => { changeset_id => $ah->result_source->schema->current_changeset },
85         });
86     }
87
88     $self->next::method($upd, @rest);
89 }
90
91 =head1 NAME
92
93 DBIx::Class::Journal - auditing for tables managed by DBIx::Class
94
95 =head1 SYNOPSIS
96
97   package My::Schema;
98   use base 'DBIx::Class::Schema';
99
100   __PACKAGE__->load_components(qw/+DBIx::Class::Schema::Journal/);
101
102   __PACKAGE__->journal_connection(['dbi:SQLite:t/var/Audit.db']);
103   __PACKAGE__->journal_user(['My::Schema::User', {'foreign.userid' => 'self.user_id'}]);
104
105
106  ########
107
108   $schema->changeset_user($user->id);
109   my $new_artist = $schema->txn_do( sub {
110    return = $schema->resultset('Artist')->create({ name => 'Fred' });
111   });
112
113
114 =head1 DESCRIPTION
115
116 The purpose of this L<DBIx::Class> component module is to create an
117 audit-trail for all changes made to the data in your database (via a
118 DBIx::Class schema). It creates changesets and assigns each
119 create/update/delete operation an id. The creation and deletion date
120 of each row is stored, as well as the previous contents of any row
121 that gets changed.
122
123 All queries which want auditing should be called using
124 L<DBIx::Class::Schema/txn_do>, which is used to create changesets for
125 each transaction.
126
127 To track who did which changes, the user_id (an integer) of the
128 current user can be set, a session_id can also be set, both are
129 optional.
130
131 To access the auditing schema to look at the auditdata or revert a
132 change, use C<< $schema->_journal_schema >>.
133
134 =head2 TABLES
135
136 The journal schema contains a number of tables. 
137
138 =over
139
140 =item ChangeSet
141
142 Each changeset row has an auto-incremented ID, optional user_id and
143 session_id, and a set_date which defaults to the current datetime.
144
145 A ChangeSet has_many Changes.
146
147 =item ChangeLog
148
149 Each change/operation done in the transaction is recorded as a row in
150 the ChangeLog table. It contains an auto-incrementing ID, the
151 changeset_id and an order column for the ordering of each change in
152 the changeset.
153
154 =item AuditLog
155
156 For every table in the original database that is to be audited, an
157 AuditLog table is created. Each auditlog row has an id which will
158 contain the primary key of the table it is associated with. (NB:
159 currently only supports integer-based single column PKs). The
160 create_id and delete_id fields contain the IDs of the Changes that
161 created or deleted this row.
162
163 =item AuditHistory
164
165 For every table in the original database to be audited, an
166 AuditHistory table is created. Each row has a change_id field
167 containing the ID of the ChangeLog row. The other fields correspond to
168 all the fields from the original table. Each time a column value in
169 the original table is changed, the entire row contents before the
170 change are added as a new row in this table.
171
172 =back
173
174 =head2 METHODS
175
176 =over
177
178 =item journal_connection \@connect_info
179
180 Set the connection information for the database to save your audit
181 information to.
182
183 Leaving this blank assumes you want to store the audit data into your current
184 database. The storage object will be shared by the regular schema and the
185 journalling schema.
186
187 =item journal_sources \@source_names
188
189 Set a list of source names you would like to audit, if unset, all
190 sources are used.
191
192 NOTE: Currently only sources with a single-column PK are supported, so
193 use this method if you have sources with multi-column PKs.
194
195 =item journal_storage_type $type
196
197 Enter the special storage type of your journal schema if needed. See
198 L<DBIx::Class::Storage::DBI> for more information on storage types.
199
200 =item journal_user \@rel
201
202 The user_id column in the L</ChangeSet> will be linked to your user id
203 with a belongs_to relation, if this is set with the appropriate
204 arguments.
205
206 =item journal_deploy_on_connect $bool
207
208 If set to a true value will cause C<journal_schema_deploy> to be called on
209 C<connect>.
210
211 Not reccomended, but present for backwards compatibility.
212
213 =item changeset_user $user_id
214
215 Set the user_id for the following changeset(s). This must be an integer.
216
217 =item changeset_session $session_id
218
219 Set the session_id for the following changeset(s). This must be an integer.
220
221 =item txn_do $code_ref, @args
222
223 Overloaded L<DBIx::Class::Schema/txn_do>, this must be used to start a
224 new changeset to cover a group of changes. Each subsequent change to
225 an audited table will use the changeset_id created in the most recent
226 txn_do call.
227
228 Currently nested C<txn_do> calls cause a single ChangeSet object to be created.
229
230 =back
231
232 =head1 SEE ALSO
233
234 L<DBIx::Class> - You'll need it to use this.
235
236 =head1 NOTES
237
238 Only single-column integer primary key'd tables are supported for auditing so far.
239
240 Updates made via L<DBIx::Class::ResultSet/update> are not yet supported.
241
242 No API for viewing or restoring changes yet.
243
244 Patches for the above welcome ;)
245
246 =head1 AUTHOR
247
248 Jess Robinson <castaway@desert-island.me.uk>
249
250 Matt S. Trout <mst@shadowcatsystems.co.uk> (ideas and prodding)
251
252 =head1 LICENCE
253
254 You may distribute this code under the same terms as Perl itself.
255
256 =cut
257
258 1;