sequel-auditer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ #
4
+ #
5
+ OSX = RUBY_PLATFORM.match(/darwin/)
6
+
7
+
8
+
9
+ Rake::TestTask.new(:spec) do |t|
10
+ t.libs << 'spec'
11
+ t.libs << 'lib'
12
+ t.test_files = FileList['spec/**/*_spec.rb']
13
+ end
14
+
15
+ task :default => :spec
16
+
17
+ desc 'Run specs with coverage'
18
+ task :coverage do
19
+ ENV['COVERAGE'] = '1'
20
+ Rake::Task['spec'].invoke
21
+ `open coverage/index.html` if OSX
22
+ end
23
+
24
+ desc 'Run Rubocop report'
25
+ task :rubocop do
26
+ res = `which rubocop`
27
+ if res != ""
28
+ `rubocop -f html -o ./rubocop/report.html lib/`
29
+ `open rubocop/report.html` if OSX
30
+ else
31
+ puts "\nERROR: 'rubocop' gem is not installed or available. Please run 'gem install rubocop'."
32
+ end
33
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sequel/auditer"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/env.test.sample ADDED
@@ -0,0 +1,7 @@
1
+ # NOTE: rename this file as '.env.test' and change YOURNAME or the full DB path(s) below
2
+
3
+ # Using PostgreSQL
4
+ DATABASE_URL="postgres://YOURNAME@localhost/sequel-audited-test"
5
+
6
+ # Using SQLite
7
+ # DATABASE_URL="sqlite://spec/sequel-audited-test.db"
@@ -0,0 +1,37 @@
1
+ require "sequel/auditer/railtie"
2
+ require "sequel/auditer/version"
3
+
4
+
5
+ module Sequel
6
+
7
+ #
8
+ module Auditer
9
+
10
+ CREATE = 'create'
11
+ UPDATE = 'update'
12
+ DESTROY = 'destroy'
13
+
14
+ # set the name of the global method that provides the current user. Default: :current_user
15
+ @auditer_current_user_method = :current_user
16
+ # set any additional info such as :ip, :user_agent, ...
17
+ @auditer_additional_info_method = :additional_info
18
+ # enable swapping of the Audit model
19
+ @auditer_model_name = :AuditLog
20
+ # toggle for enabling / disabling auditing
21
+ @auditer_enabled = true
22
+
23
+ # by default ignore these columns
24
+ @auditer_default_ignored_columns = [
25
+ # :id, :ref, :password, :password_hash,
26
+ :lock_version,
27
+ :created_at, :updated_at, :created_on, :updated_on
28
+ ]
29
+
30
+ class << self
31
+ attr_accessor :auditer_current_user_method, :auditer_additional_info_method,
32
+ :auditer_model_name, :auditer_enabled,
33
+ :auditer_default_ignored_columns
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ module Sequel
2
+ module Auditer
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ Sequel::Auditer::Railtie.env = env
10
+ @app.call(env)
11
+ end
12
+ end
13
+
14
+ class Railtie < ::Rails::Engine
15
+ initializer "sequel-auditer_railtie.configure_rails_initialization" do |app|
16
+ app.middleware.use Sequel::Auditer::Middleware
17
+ end
18
+ attr_accessor :env
19
+
20
+ def self.user
21
+ return env['warden'].user if env && env.key?('warden')
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ module Sequel
2
+ module Auditer
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,325 @@
1
+ require_relative '../auditer'
2
+
3
+ class AuditLog < Sequel::Model
4
+ # handle versioning of audited records
5
+ plugin :list, field: :version, scope: [:associated_type, :associated_id]
6
+ plugin :timestamps
7
+ plugin :polymorphic
8
+
9
+ # TODO: see if we should add these
10
+ many_to_one :associated, polymorphic: true
11
+ many_to_one :modifier, polymorphic: true
12
+
13
+ def before_validation
14
+ # grab the current user
15
+ if u = audit_user
16
+ self.modifier = u
17
+ end
18
+
19
+ # grab any additional info if any
20
+ if i = audit_additional_info
21
+ self.additional_info = i
22
+ end
23
+
24
+ super
25
+ end
26
+
27
+ # private
28
+
29
+ # Obtains the `current_user` based upon the `:auditer_current_user_method' value set in the
30
+ # audited model, either via defaults or via :user_method config options
31
+ #
32
+ # # NOTE! this allows overriding the default value on a per audited model
33
+ def audit_user
34
+ user = ::Sequel::Auditer::Railtie.user
35
+ return user if !user.nil?
36
+
37
+ begin
38
+ m = Kernel.const_get(associated_type)
39
+ m.send(m.auditer_current_user_method) || send(m.auditer_current_user_method)
40
+ rescue
41
+ nil
42
+ end
43
+ end
44
+
45
+ def audit_additional_info
46
+ begin
47
+ m = Kernel.const_get(associated_type)
48
+ m.send(m.auditer_additional_info_method) || send(m. auditer_additional_info_method)
49
+ rescue
50
+ nil
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ module Sequel
57
+ module Plugins
58
+
59
+ # Given a Post model with these fields:
60
+ # [:id, :category_id, :title, :body, :author_id, :created_at, :updated_at]
61
+ #
62
+ #
63
+ # All fields
64
+ # plugin :auditer
65
+ # #=> [:category_id, :title, :body, :author_id] # NB! excluding @default_ignore_attrs
66
+ # #=> [:id, :created_at, :updated_at]
67
+ #
68
+ # Single field
69
+ # plugin :auditer, only: :title
70
+ # plugin :auditer, only: [:title]
71
+ # #=> [:title]
72
+ # #+> [:id, :category_id, :body, :author_id, :created_at, :updated_at] # ignored fields
73
+ #
74
+ # Multiple fields
75
+ # plugin :auditer, only: [:title, :body]
76
+ # #=> [:title, :body] # tracked fields
77
+ # #=> [:id, :category_id, :author_id, :created_at, :updated_at] # ignored fields
78
+ #
79
+ #
80
+ # All fields except certain fields
81
+ # plugin :auditer, except: :title
82
+ # plugin :auditer, except: [:title]
83
+ # #=> [:id, :category_id, :author_id, :created_at, :updated_at] # tracked fields
84
+ # #=> [:title] # ignored fields
85
+ #
86
+ #
87
+ #
88
+ module Auditer
89
+
90
+ # called when
91
+ def self.configure(model, opts = {})
92
+ model.instance_eval do
93
+ # add support for :dirty attributes tracking & JSON serializing of data
94
+ plugin(:dirty)
95
+ plugin(:json_serializer)
96
+ plugin(:polymorphic)
97
+
98
+ # set the default ignored columns or revert to defaults
99
+ set_default_ignored_columns(opts)
100
+ # sets the name of the current User method or revert to default: :current_user
101
+ # specifically for the audited model on a per model basis
102
+ set_user_method(opts)
103
+ set_additional_info_method(opts)
104
+
105
+ set_reference_method(opts)
106
+
107
+ only = opts.fetch(:only, [])
108
+ except = opts.fetch(:except, [])
109
+
110
+ unless only.empty?
111
+ # we should only track the provided column
112
+ included_columns = [only].flatten
113
+ # subtract the 'only' columns from all columns to get excluded_columns
114
+ excluded_columns = columns - included_columns
115
+ else # except:
116
+ # all columns minus any excepted columns and default ignored columns
117
+ included_columns = [
118
+ [columns - [except].flatten].flatten - @auditer_default_ignored_columns
119
+ ].flatten.uniq
120
+
121
+ # except_columns = except.empty? ? [] : [except].flatten
122
+ excluded_columns = [columns - included_columns].flatten.uniq
123
+ # excluded_columns = [columns - [except_columns, included_columns].flatten].flatten.uniq
124
+ end
125
+
126
+ @auditer_included_columns = included_columns
127
+ @auditer_ignored_columns = excluded_columns
128
+
129
+ # each included model will have an associated versions
130
+ one_to_many(
131
+ :versions,
132
+ class: audit_model_name,
133
+ as: 'associated'
134
+ )
135
+
136
+ end
137
+
138
+
139
+ end
140
+
141
+ #
142
+ module ClassMethods
143
+
144
+ attr_accessor :auditer_default_ignored_columns, :auditer_current_user_method, :auditer_additional_info_method
145
+ # The holder of ignored columns
146
+ attr_reader :auditer_ignored_columns
147
+ # The holder of columns that should be audited
148
+ attr_reader :auditer_included_columns
149
+
150
+ attr_accessor :auditer_reference_method
151
+
152
+
153
+ Plugins.inherited_instance_variables(self,
154
+ :@auditer_default_ignored_columns => nil,
155
+ :@auditer_current_user_method => nil,
156
+ :@auditer_additional_info_method => nil,
157
+ :@auditer_included_columns => nil,
158
+ :@auditer_ignored_columns => nil,
159
+ :@auditer_reference_method => nil
160
+ )
161
+
162
+ def non_audited_columns
163
+ columns - auditer_columns
164
+ end
165
+
166
+ def auditer_columns
167
+ @auditer_columns ||= columns - @auditer_ignored_columns
168
+ end
169
+
170
+ # def default_ignored_attrs
171
+ # # TODO: how to reference the models primary_key value??
172
+ # arr = [pk.to_s]
173
+ # # handle STI (Class Table Inheritance) models with `plugin :single_table_inheritance`
174
+ # arr << 'sti_key' if self.respond_to?(:sti_key)
175
+ # arr
176
+ # end
177
+
178
+ #
179
+ # returns true / false if any audits have been made
180
+ #
181
+ # Post.auditer_versions? #=> true / false
182
+ #
183
+ def auditer_versions?
184
+ audit_model.where(associated_type: name.to_s).count >= 1
185
+ end
186
+
187
+ # grab all audits for a particular model based upon filters
188
+ #
189
+ # Posts.auditer_versions(:model_pk => 123)
190
+ # #=> filtered by primary_key value
191
+ #
192
+ # Posts.auditer_versions(:user_id => 88)
193
+ # #=> filtered by user name
194
+ #
195
+ # Posts.auditer_versions(:created_at < Date.today - 2)
196
+ # #=> filtered to last two (2) days only
197
+ #
198
+ # Posts.auditer_versions(:created_at > Date.today - 7)
199
+ # #=> filtered to older than last seven (7) days
200
+ #
201
+ def auditer_versions(opts = {})
202
+ audit_model.where(opts.merge(associated_type: name.to_s)).order(:version).all
203
+ end
204
+
205
+
206
+ private
207
+
208
+
209
+ def audit_model
210
+ const_get(audit_model_name)
211
+ end
212
+
213
+ def audit_model_name
214
+ ::Sequel::Auditer.auditer_model_name
215
+ end
216
+
217
+ def set_default_ignored_columns(opts)
218
+ if opts[:default_ignored_columns]
219
+ @auditer_default_ignored_columns = opts[:default_ignored_columns]
220
+ else
221
+ @auditer_default_ignored_columns = ::Sequel::Auditer.auditer_default_ignored_columns
222
+ end
223
+ end
224
+
225
+ def set_user_method(opts)
226
+ if opts[:user_method]
227
+ @auditer_current_user_method = opts[:user_method]
228
+ else
229
+ @auditer_current_user_method = ::Sequel::Auditer.auditer_current_user_method
230
+ end
231
+ end
232
+
233
+ def set_additional_info_method(opts)
234
+ if opts[:additional_info]
235
+ @auditer_additional_info_method = opts[:additional_info]
236
+ else
237
+ @auditer_additional_info_method = ::Sequel::Auditer.auditer_additional_info_method
238
+ end
239
+ end
240
+
241
+ def set_reference_method(opts)
242
+ if opts[:reference_method]
243
+ @auditer_reference_method = opts[:reference_method]
244
+ end
245
+ end
246
+
247
+ end
248
+
249
+
250
+ #
251
+ module InstanceMethods
252
+
253
+ # Returns who put the post into its current state.
254
+ #
255
+ # post.blame # => 'joeblogs'
256
+ #
257
+ # post.last_audited_by # => 'joeblogs'
258
+ #
259
+ # Note! returns 'not audited' if there's no audited version (new unsaved record)
260
+ #
261
+ def blame
262
+ v = versions.last unless versions.empty?
263
+ v ? v.modifier : 'not audited'
264
+ end
265
+ alias_method :last_audited_by, :blame
266
+
267
+ # Returns who put the post into its current state.
268
+ #
269
+ # post.last_audited_at # => '2015-12-19 @ 08:24:45'
270
+ #
271
+ # post.last_audited_on # => 'joeblogs'
272
+ #
273
+ # Note! returns 'not audited' if there's no audited version (new unsaved record)
274
+ #
275
+ def last_audited_at
276
+ v = versions.last unless versions.empty?
277
+ v ? v.created_at : 'not audited'
278
+ end
279
+ alias_method :last_audited_on, :last_audited_at
280
+
281
+ private
282
+
283
+ # extract audited values only
284
+ def auditer_values(event)
285
+ vals = case event
286
+ when Sequel::Auditer::CREATE
287
+ self.values
288
+ when Sequel::Auditer::UPDATE
289
+ (column_changes.empty? ? previous_changes : column_changes)
290
+ when Sequel::Auditer::DESTROY
291
+ self.values
292
+ end
293
+ vals.except(*model.auditer_default_ignored_columns)
294
+ end
295
+
296
+ def add_audited(event)
297
+ changed = auditer_values(event)
298
+ unless changed.blank?
299
+ add_version(
300
+ event: event,
301
+ changed: changed
302
+ )
303
+ end
304
+ end
305
+
306
+ ### CALLBACKS ###
307
+
308
+ def after_create
309
+ super
310
+ add_audited(Sequel::Auditer::CREATE)
311
+ end
312
+
313
+ def after_update
314
+ super
315
+ add_audited(Sequel::Auditer::UPDATE)
316
+ end
317
+
318
+ def after_destroy
319
+ super
320
+ add_audited(Sequel::Auditer::DESTROY)
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end