hoodoo 2.5.1 → 2.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: bcd6275929dde876479da0087f2eef17e4d9fd3c8606182daeff2fd4c02c35c9
4
- data.tar.gz: 3763e2d462802bbdae8240864e7bf73b83eb5458675da127af70edba8013dc1b
2
+ SHA1:
3
+ metadata.gz: cbae2491898cb113ffb0d3b9580e860e050d0fd5
4
+ data.tar.gz: d7dd9c51af9a0015572d9cb46d5a0fb63ee679c4
5
5
  SHA512:
6
- metadata.gz: 6492416bb7afa5d8e7d9a23169293f055c73a6b9e8c63362f65b08ce7f49bd219874be90480ea7156c02373725e8088b19f9dfdda3a9ba2329b3037b7bc8b958
7
- data.tar.gz: a8142c1c0973b77b1101af5f757b088b81a11bac48cce8597691288dcc288c8a786efd1ea6dd16820c44b9c222fb8df351cb739e6a2a6ea3e248f09b3943bacb
6
+ metadata.gz: 1e2bf59ec7c80eb94e45a3cb549f18f783b4fce0d5b263c4da9b92b7bf01c69b32f8a0980f5ce25f7a7ebc4bff5f0d60d63e2b51dea93784491382abc58f0571
7
+ data.tar.gz: 0a55cfb887c0fbf48594081bc2cd44e51b48c3e46dbc22018937acfc6645caca60e1981fcc3f49dea5883b8a925ae37dc1e9c2972e88472ae7436d7957651795
@@ -393,8 +393,9 @@ module Hoodoo
393
393
  )
394
394
 
395
395
  unless model == Hoodoo::ActiveRecord::Base
396
- model.send( :include, Hoodoo::ActiveRecord::UUID )
396
+ model.send( :include, Hoodoo::ActiveRecord::UUID )
397
397
  model.send( :include, Hoodoo::ActiveRecord::Finder )
398
+
398
399
  instantiate( model )
399
400
  end
400
401
 
@@ -9,6 +9,8 @@
9
9
  # 25-Nov-2014 (ADH): Created.
10
10
  ########################################################################
11
11
 
12
+ require 'hoodoo/active/active_record/security_helper'
13
+
12
14
  module Hoodoo
13
15
  module ActiveRecord
14
16
 
@@ -58,6 +60,16 @@ module Hoodoo
58
60
  model.extend( ClassMethods )
59
61
  end
60
62
 
63
+ # Convenience constant defining an equals-single-security-value wildcard
64
+ # security exemption using the String '*'.
65
+ #
66
+ OBJECT_EQLS_STAR = Hoodoo::ActiveRecord::Secure::SecurityHelper::eqls_wildcard( '*' )
67
+
68
+ # Convenience constant defining an included-in-enumerable-security-value
69
+ # wildcard security excemption using the String '*'.
70
+ #
71
+ ENUMERABLE_INCLUDES_STAR = Hoodoo::ActiveRecord::Secure::SecurityHelper::includes_wildcard( '*' )
72
+
61
73
  # Collection of class methods that get defined on an including class via
62
74
  # Hoodoo::ActiveRecord::Secure::included.
63
75
  #
@@ -69,7 +81,9 @@ module Hoodoo
69
81
  # alternative argument generator is given in the longhand form's
70
82
  # value Hash's +:using+ key.
71
83
  #
72
- DEFAULT_SECURE_PROC = Proc.new { | model_class, database_column_name, session_field_value | [ { database_column_name => session_field_value } ] }
84
+ DEFAULT_SECURE_PROC = Proc.new do | model_class, database_column_name, session_field_value |
85
+ [ { database_column_name => session_field_value } ]
86
+ end
73
87
 
74
88
  # The core of out-of-the-box Hoodoo data access security layer.
75
89
  #
@@ -255,6 +269,9 @@ module Hoodoo
255
269
  # +using+:: See the _Advanced_ _query_ _conditions_
256
270
  # section later for details.
257
271
  #
272
+ # +exemptions+:: See the _Security_ _exemptions_ section later
273
+ # for details.
274
+ #
258
275
  # To help clarify the above, the following two calls to #secure_with
259
276
  # have exactly the same effect.
260
277
  #
@@ -320,7 +337,7 @@ module Hoodoo
320
337
  #
321
338
  # This leads to SQL along the following lines:
322
339
  #
323
- # AND ("model_table"."creating_caller_uuid" = '[val]')
340
+ # AND ("model_table"."creating_caller_uuid" IN ('[val]'))
324
341
  #
325
342
  # ...where <tt>val</tt> is from the Session +authorised_caller_uuids+
326
343
  # data in the +scoping+ section (so this might be an SQL +IN+ rather
@@ -352,7 +369,7 @@ module Hoodoo
352
369
  #
353
370
  # ...yields something like:
354
371
  #
355
- # AND ( "model_table"."creating_caller_uuid" = '[val]' OR "model_table"."other_column_name" = '[val]' )
372
+ # AND ( "model_table"."creating_caller_uuid" IN ('[val]') OR "model_table"."other_column_name" IN ('[val]') )
356
373
  #
357
374
  # A Proc specified with +:using+ is called with:
358
375
  #
@@ -369,6 +386,92 @@ module Hoodoo
369
386
  # +where+ via <tt>where( *returned_values )</tt> as part of the wider
370
387
  # query chain.
371
388
  #
389
+ # == Security exemptions
390
+ #
391
+ # Sometimes you might want a security bypass mechanism for things like
392
+ # a Superuser style caller that can "see everything". It's more secure,
393
+ # where possible and scalable, to simply have the session data match
394
+ # every known value of some particular secured-with quantity, but this
395
+ # might get unwieldy. "WHERE IN" queries with hundreds or thousands of
396
+ # listed items can cause problems!
397
+ #
398
+ # Noting that with any security exemption there is elevated risk, you
399
+ # can use the +:exemptions+ key to provide a Proc which is passed the
400
+ # secure value(s) under consideration (the data taken directly from
401
+ # the session scoping section) and evaluates to +true+ if the value(s)
402
+ # indicate that a security exemption applies, else evaluates "falsey"
403
+ # for normal behaviour. We say "value(s)" here as a single key used to
404
+ # read from the scoping section of a session may yield either a simple
405
+ # value such as a String, or an Enumerable object such as an array of
406
+ # many Strings.
407
+ #
408
+ # If the Proc evaluates to +true+, the result is no modification to the
409
+ # secure scope chain being constructed for the secured ActiveRecord
410
+ # query the caller will eventually run. Helper methods which construct
411
+ # common use case Procs are present in
412
+ # Hoodoo::ActiveRecord::Secure::SecurityHelper and there are
413
+ # convenience constants defined in Hoodoo::ActiveRecord::Secure, such
414
+ # as Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR.
415
+ #
416
+ # Taking an earlier example:
417
+ #
418
+ # secure_with( {
419
+ # :creating_caller_uuid => :authorised_caller_uuids
420
+ # } )
421
+ #
422
+ # # ...has this minimal longhand equivalent...
423
+ #
424
+ # secure_with( {
425
+ # :creating_caller_uuid => {
426
+ # :session_field_name => :authorised_caller_uuids
427
+ # }
428
+ # } )
429
+ #
430
+ # ...which leads to SQL along the following lines:
431
+ #
432
+ # AND ("model_table"."creating_caller_uuid" IN ('[val]'))
433
+ #
434
+ # ...then suppose we wanted to allow a session scoping value of '*'
435
+ # bypass security ("see everything"). We could use the
436
+ # Enumerable-includes-star matcher Proc
437
+ # Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR here. At the
438
+ # time of writing, it is defined as the following Proc:
439
+ #
440
+ # Proc.new do | security_values |
441
+ # security_values.is_a?( Enumerable ) &&
442
+ # security_values.include?( '*' ) rescue false
443
+ # end
444
+ #
445
+ # This is activated through the +:exemptions+ key:
446
+ #
447
+ # secure_with( {
448
+ # :creating_caller_uuid => {
449
+ # :session_field_name => :authorised_caller_uuids,
450
+ # :exemptions => Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR
451
+ # }
452
+ # } )
453
+ #
454
+ # If the looked up value of the +authorised_caller_uuids+ attribute
455
+ # in the prevailing Session scoping section data was ["1234"], then the
456
+ # SQL query additions would occur as above:
457
+ #
458
+ # AND ("model_table"."creating_caller_uuid" IN ('1234'))
459
+ #
460
+ # ...but if there is a value of "*", the security layer will ignore the
461
+ # normal restrictions, resulting in no SQL additions whatsoever.
462
+ #
463
+ # Since a Proc is used to compare the data found in the session against
464
+ # some wildcard, things like checking an array of values for some magic
465
+ # bypass characters / key, using regular expression matching, or other
466
+ # more heavyweight options are all possible. Remember, though, that all
467
+ # of this comes at a risk, since the mechanism is bypassing the normal
468
+ # scope chain security. If used improperly or somehow compromised, it
469
+ # will allow data to be read by an API caller that should not have been
470
+ # permitted to access it.
471
+ #
472
+ # See module Hoodoo::ActiveRecord::Secure::SecurityHelper for methods
473
+ # to help with exemption Proc construction.
474
+ #
372
475
  def secure( context )
373
476
  prevailing_scope = all() # "Model.all" -> returns anonymous scope
374
477
  extra_scope_map = secured_with()
@@ -377,25 +480,33 @@ module Hoodoo
377
480
  return none() if context.session.nil? || context.session.scoping.nil?
378
481
 
379
482
  extra_scope_map.each do | model_field_name, key_or_options |
380
- params_proc = DEFAULT_SECURE_PROC
483
+ exemption_proc = nil
484
+ params_proc = DEFAULT_SECURE_PROC
381
485
 
382
486
  if key_or_options.is_a?( Hash )
383
487
  session_scoping_key = key_or_options[ :session_field_name ]
488
+ exemption_proc = key_or_options[ :exemptions ]
384
489
  params_proc = key_or_options[ :using ] if key_or_options.has_key?( :using )
385
490
  else
386
491
  session_scoping_key = key_or_options
387
492
  end
388
493
 
389
494
  if context.session.scoping.respond_to?( session_scoping_key )
390
- args = params_proc.call(
391
- self,
392
- model_field_name,
393
- context.session.scoping.send( session_scoping_key )
394
- )
395
- prevailing_scope = prevailing_scope.where( *args )
495
+ security_value = context.session.scoping.send( session_scoping_key )
496
+
497
+ if exemption_proc.nil? || exemption_proc.call( security_value ) != true
498
+ args = params_proc.call(
499
+ self,
500
+ model_field_name,
501
+ security_value
502
+ )
503
+ prevailing_scope = prevailing_scope.where( *args )
504
+ end
505
+
396
506
  else
397
507
  prevailing_scope = none()
398
508
  break
509
+
399
510
  end
400
511
  end
401
512
  end
@@ -0,0 +1,143 @@
1
+ ########################################################################
2
+ # File:: security_helper.rb
3
+ # (C):: Loyalty New Zealand 2018
4
+ #
5
+ # Purpose:: Supplementary helper class included by "finder.rb". See
6
+ # Hoodoo::ActiveRecord::Secure, especially
7
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with and
8
+ # its options Hash, for details.
9
+ # ----------------------------------------------------------------------
10
+ # 05-Apr-2018 (ADH): Created.
11
+ ########################################################################
12
+
13
+ module Hoodoo
14
+ module ActiveRecord
15
+ module Secure
16
+
17
+ # Help build security exemption Procs to pass into
18
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with via its options
19
+ # Hash. The following extends an example given in the documentation (at
20
+ # the time of writing here) for the underlying implementation method
21
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure:
22
+ #
23
+ # class Audit < ActiveRecord::Base
24
+ # include Hoodoo::ActiveRecord::Secure
25
+ #
26
+ # secure_with(
27
+ # {
28
+ # :creating_caller_uuid => :authorised_caller_uuids
29
+ # },
30
+ # {
31
+ # :exemptions => Hoodoo::ActiveRecord::Secure::SecurityHelper::includes_wildcard( '*' )
32
+ # }
33
+ # )
34
+ # end
35
+ #
36
+ # Note that the Hoodoo::ActiveRecord::Secure module includes some belper
37
+ # constants to aid brevity for common cases such as the single value
38
+ # <tt>#eql?</tt> or enumerable <tt>#include?</tt> matchers checking for
39
+ # a '*' as an indiscriminate wildcard - see for example
40
+ # Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR.
41
+ #
42
+ class SecurityHelper
43
+
44
+ # Internally used by ::matches_wildcard for Ruby 2.4.0+ performance.
45
+ #
46
+ RUBY_FAST_WILDCARD_PROC_CONTENTS = %q{
47
+ security_value.match?( wildcard_regexp ) rescue false
48
+ }
49
+
50
+ # Internally used by ::matches_wildcard for Ruby < 2.4 compatibility.
51
+ #
52
+ RUBY_SLOW_WILDCARD_PROC_CONTENTS = %q{
53
+ wildcard_regexp.match( security_value ) != nil rescue false
54
+ }
55
+
56
+ # Match a given wildcard, typically a String, to a single value
57
+ # via <tt>#eql?</tt>.
58
+ #
59
+ # +wildcard_value+:: Wildcard value to match, e.g. <tt>'*'</tt>.
60
+ #
61
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
62
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
63
+ #
64
+ def self.eqls_wildcard( wildcard_value )
65
+ Proc.new do | security_value |
66
+ security_value.eql?( wildcard_value ) rescue false
67
+ end
68
+ end
69
+
70
+ # Match a given wildcard, typically a String, inside an Enumerable
71
+ # subclass via <tt>#include?</tt>.
72
+ #
73
+ # +wildcard_value+:: Wildcard value to match, e.g. <tt>'*'</tt>.
74
+ #
75
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
76
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
77
+ #
78
+ def self.includes_wildcard( wildcard_value )
79
+ Proc.new do | security_values |
80
+ security_values.is_a?( Enumerable ) &&
81
+ security_values.include?( wildcard_value ) rescue false
82
+ end
83
+ end
84
+
85
+ # Match a given wildcard Regexp to a value via <tt>#match?</tt>.
86
+ #
87
+ # +wildcard_value+:: Wildcard Regexp to use, e.g. <tt>/.*/</tt>.
88
+ # Strings are coerced to Regexps without any
89
+ # escaping but doing so reduces performance.
90
+ #
91
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
92
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
93
+ #
94
+ def self.matches_wildcard( wildcard_regexp )
95
+ wildcard_regexp = Regexp.new( wildcard_regexp ) unless wildcard_regexp.is_a?( Regexp )
96
+
97
+ # Use security_value's #match? (if present) to ensure that we have
98
+ # an expected "matchable" type. This is only available in Ruby 2.4
99
+ # or later, so a patch is performed below for earlier Rubies.
100
+ #
101
+ Proc.new do | security_value |
102
+
103
+ # Ruby 2.4.0 and later introduce the Regexp#match? family, which
104
+ # is the fastest way to determine a simple does-or-does-not match
105
+ # condition. Ruby 2.3.x and earlier need different, slower code.
106
+ #
107
+ if ''.respond_to?( :match? )
108
+ eval( RUBY_FAST_WILDCARD_PROC_CONTENTS )
109
+ else
110
+ eval( RUBY_SLOW_WILDCARD_PROC_CONTENTS )
111
+ end
112
+ end
113
+ end
114
+
115
+ # Match a given wildcard Regexp to any value in an enumerable
116
+ # object via iteration and <tt>#match?</tt>. Exists with +true+
117
+ # as soon as any match is made.
118
+ #
119
+ # +wildcard_value+:: Wildcard Regexp to use, e.g. <tt>/.*/</tt>.
120
+ # Strings are coerced to Regexps without any
121
+ # escaping but doing so reduces performance.
122
+ #
123
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
124
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
125
+ #
126
+ def self.matches_wildcard_enumerable( wildcard_regexp )
127
+ match_proc = self.matches_wildcard( wildcard_regexp )
128
+
129
+ Proc.new do | security_values |
130
+ begin
131
+ security_values.any? do | security_value |
132
+ match_proc.call( security_value )
133
+ end
134
+ rescue
135
+ false
136
+ end
137
+ end
138
+ end
139
+
140
+ end
141
+ end
142
+ end
143
+ end
@@ -41,12 +41,17 @@ module Hoodoo
41
41
  # # ...
42
42
  # end
43
43
  #
44
+ # Depends upon and auto-includes Hoodoo::ActiveRecord::Creator
45
+ # and Hoodoo::ActiveRecord::ErrorMapping.
46
+ #
44
47
  # +model+:: The ActiveRecord::Base descendant that is including
45
48
  # this module.
46
49
  #
47
50
  def self.included( model )
48
51
  unless model == Hoodoo::ActiveRecord::Base
52
+ model.send( :include, Hoodoo::ActiveRecord::Creator )
49
53
  model.send( :include, Hoodoo::ActiveRecord::ErrorMapping )
54
+
50
55
  instantiate( model )
51
56
  end
52
57
 
@@ -75,18 +80,93 @@ module Hoodoo
75
80
  end
76
81
  end
77
82
 
78
- # Instance equivalent of
79
- # Hoodoo::ActiveRecord::Writer::ClassMethods.persist_in - see that for
80
- # details. The class method just calls here, having constructed an
81
- # instance based on the attributes it was given. If you have already
82
- # built an instance yourself, just call this instance method equivalent
83
- # instead.
84
- #
85
- # As an instance-based method, the return value and error handling
86
- # semantics differ from the class-based counterpart. Instead of
87
- # checking "persisted?", check the return value of +persist_in+. This
88
- # means you can also use +persist_in+ to save a previously persisted,
89
- # but now updated record, should you so wish.
83
+ # == Overview
84
+ #
85
+ # Service authors _SHOULD_ use this method when persisting data with
86
+ # ActiveRecord if there is a risk of duplication constraint violation
87
+ # of any kind. This will include a violation on the UUID of a resource
88
+ # if you support external setting of this value via the body of a
89
+ # +create+ call containing the +id+ field, injected by Hoodoo as the
90
+ # result of an authorised use of the <tt>X-Resource-UUID</tt> HTTP
91
+ # header.
92
+ #
93
+ # You can use this method for both persisting new records or
94
+ # persisting updates, in the same way as ActiveRecord's +save+ is
95
+ # used for either.
96
+ #
97
+ #
98
+ # == Concurrency
99
+ #
100
+ # Services often run in highly concurrent environments and uniqueness
101
+ # constraint validations with ActiveRecord cannot protect against
102
+ # race conditions in such cases. Those work at the application level;
103
+ # the check to see if a record exists with a duplicate value in some
104
+ # given column is a separate operation from that which stores the
105
+ # record subsequently. As per the Rails Guides entry on the uniqueness
106
+ # validation at the time of writing:
107
+ #
108
+ # http://guides.rubyonrails.org/active_record_validations.html#uniqueness
109
+ #
110
+ # <i>"It does not create a uniqueness constraint in the database, so
111
+ # it may happen that two different database connections create two
112
+ # records with the same value for a column that you intend to be
113
+ # unique. To avoid that, you must create a unique index on both
114
+ # columns in your database."</i>
115
+ #
116
+ # You *MUST* always use a uniqueness constraint at the database level
117
+ # and *MAY* additionally use ActiveRecord validations for a higher
118
+ # level warning in all but race condition edge cases. If you then use
119
+ # this +persist_in+ method to store records, all duplication cases
120
+ # will be handled elegantly and reported as a
121
+ # <tt>generic.invalid_duplication</tt> error. In the event that a
122
+ # caller has used the <tt>X-Deja-Vu</tt> HTTP header, Hoodoo will take
123
+ # such an error and transform it into a non-error 204 HTTP response;
124
+ # so by using +persist_in+, you also ensure that your service
125
+ # participates successfully in this process without any additional
126
+ # coding effort. You get safe concurrency and protection against the
127
+ # inherent lack of idempotency in HTTP +POST+ operations via any
128
+ # must-be-unique fields (within your defined scope) automatically.
129
+ #
130
+ # Using this method for data storage instead of plain ActiveRecord
131
+ # +save+ or <tt>save!</tt> will also help your code auto-inherit any
132
+ # additional future write-related enhancements in Hoodoo should they
133
+ # arise, without necessarily needing service code changes.
134
+ #
135
+ #
136
+ # == Parameters
137
+ #
138
+ # +context+:: Hoodoo::Services::Context instance describing a call
139
+ # context. This is typically a value passed to one of
140
+ # the Hoodoo::Services::Implementation instance methods
141
+ # that a resource subclass implements.
142
+ #
143
+ # Returns a Symbol of +:success+ or +:failure+ indicating the outcome
144
+ # of the same attempt. In the event of failure, the model will be
145
+ # invalid and not persisted; you can read errors immediately and should
146
+ # avoid unnecessarily re-running validations by calling +valid?+ or
147
+ # +validate+ on the instance.
148
+ #
149
+ #
150
+ # == Example
151
+ #
152
+ # class Unique < ActiveRecord::Base
153
+ # include Hoodoo::ActiveRecord::Writer
154
+ # validates :unique_code, :presence => true, :uniqueness => true
155
+ # end
156
+ #
157
+ # The migration to create the table for the Unique model _MUST_ have a
158
+ # uniqueness constraint on the +unique_code+ field, e.g.:
159
+ #
160
+ # def change
161
+ # add_column :uniques, :unique_code, :null => false
162
+ # add_index :uniques, [ :unique_code ], :unique => true
163
+ # end
164
+ #
165
+ # Then, inside the implementation class which uses the above model,
166
+ # where you have (say) written private methods +mapping_of+ which
167
+ # maps +context.request.body+ to an attributes Hash for persistence
168
+ # and +rendering_of+ which uses Hoodoo::Presenters::Base.render_in to
169
+ # properly render a representation of your resource, you would write:
90
170
  #
91
171
  # def create( context )
92
172
  # attributes = mapping_of( context.request.body )
@@ -109,18 +189,49 @@ module Hoodoo
109
189
  # context.response.set_resource( rendering_of( context, model_instance ) )
110
190
  # end
111
191
  #
112
- # Parameters:
113
192
  #
114
- # +context+:: Hoodoo::Services::Context instance describing a call
115
- # context. This is typically a value passed to one of
116
- # the Hoodoo::Services::Implementation instance methods
117
- # that a resource subclass implements.
193
+ # == See also
118
194
  #
119
- # Returns a Symbol of +:success+ or +:failure+ indicating the outcome
120
- # of the same attempt. In the event of failure, the model will be
121
- # invalid and not persisted; you can read errors immediately and should
122
- # avoid unnecessarily re-running validations by calling +valid?+ or
123
- # +validate+ on the instance.
195
+ # There is a class method equivalent which combines creating a new record
196
+ # and persisting it in a single call. If you prefer that code style, see
197
+ # Hoodoo::ActiveRecord::Writer::ClassMethods.persist_in. In such cases,
198
+ # it could look quite odd to mix the class method and instance method
199
+ # variants for new records or existing record updates; as syntax sugar,
200
+ # an alias of the #persist_in instance method is available under the name
201
+ # #update_in, so that you can use the class method for creation and the
202
+ # aliased instance method for updates.
203
+ #
204
+ #
205
+ # == Nested transaction note
206
+ #
207
+ # Ordinarily an exception in a nested transaction does not roll back.
208
+ # ActiveRecord wraps all saves in a transaction "out of the box", so
209
+ # the following construct could have unexpected results...
210
+ #
211
+ # Model.transaction do
212
+ # instance.persist_in( context )
213
+ # end
214
+ #
215
+ # ...if <tt>instance.valid?</tt> runs any SQL queries - which is very
216
+ # likely. PostgreSQL, for example, would then raise an exception; the
217
+ # inner transaction failed, leaving the outer one in an aborted state:
218
+ #
219
+ # PG::InFailedSqlTransaction: ERROR: current transaction is
220
+ # aborted, commands ignored until end of transaction block
221
+ #
222
+ # ActiveRecord provides us with a way to define a transaction that
223
+ # does roll back via the <tt>requires_new: true</tt> option. Hoodoo
224
+ # thus protects callers from the above artefacts by ensuring that all
225
+ # saves are wrapped in an outer transaction that causes rollback in
226
+ # any parents. This sidesteps the unexpected behaviour, but service
227
+ # authors might sometimes need to be aware of this if using complex
228
+ # transaction behaviour along with <tt>persist_in</tt>.
229
+ #
230
+ # In pseudocode, the internal implementation is:
231
+ #
232
+ # self.transaction( :requires_new => true ) do
233
+ # self.save
234
+ # end
124
235
  #
125
236
  def persist_in( context )
126
237
 
@@ -171,77 +282,28 @@ module Hoodoo
171
282
  return errors_occurred.nil? ? :success : :failure
172
283
  end
173
284
 
285
+ # Alias of #persist_in. Although that can be used for new records or
286
+ # updates, it's nice to have the syntax sugar of an "update in context"
287
+ # method to sit alongside things like #persist_in and
288
+ # Hoodoo::ActiveRecord::Creator::ClassMethods::new_in.
289
+ #
290
+ alias_method :update_in, :persist_in
291
+
174
292
  # Collection of class methods that get defined on an including class via
175
293
  # Hoodoo::ActiveRecord::Writer::included.
176
294
  #
177
295
  module ClassMethods
178
296
 
179
- # == Overview
180
- #
181
- # Service authors _SHOULD_ use this method when persisting data with
182
- # ActiveRecord if there is a risk of duplication constraint violation
183
- # of any kind. This will include a violation on the UUID of a resource
184
- # if you support external setting of this value via the body of a
185
- # +create+ call containing the +id+ field, injected by Hoodoo as the
186
- # result of an authorised use of the <tt>X-Resource-UUID</tt> HTTP
187
- # header.
188
- #
189
- # Services often run in highly concurrent environments and uniqueness
190
- # constraint validations with ActiveRecord cannot protect against
191
- # race conditions in such cases. IT works at the application level;
192
- # the check to see if a record exists with a duplicate value in some
193
- # given column is a separate operation from that which stores the
194
- # record subsequently. As per the Rails Guides entry on the uniqueness
195
- # validation at the time of writing:
196
- #
197
- # http://guides.rubyonrails.org/active_record_validations.html#uniqueness
198
- #
199
- # <i>"It does not create a uniqueness constraint in the database, so
200
- # it may happen that two different database connections create two
201
- # records with the same value for a column that you intend to be
202
- # unique. To avoid that, you must create a unique index on both
203
- # columns in your database."</i>
204
- #
205
- # You *MUST* always use a uniqueness constraint at the database level
206
- # and *MAY* additionally use ActiveRecord validations for a higher
207
- # level warning in all but race condition edge cases. If you then use
208
- # this +persist_in+ method to store records, all duplication cases
209
- # will be handled elegantly and reported as a
210
- # <tt>generic.invalid_duplication</tt> error. In the event that a
211
- # caller has used the <tt>X-Deja-Vu</tt> HTTP header, Hoodoo will take
212
- # such an error and transform it into a non-error 204 HTTP response;
213
- # so by using +persist_in+, you also ensure that your service
214
- # participates successfully in this process without any additional
215
- # coding effort. You get safe concurrency and protection against the
216
- # inherent lack of idempotency in HTTP +POST+ operations via any
217
- # must-be-unique fields (within your defined scope) automatically.
297
+ # A class-based equivalent of the
298
+ # Hoodoo::ActiveRecord::Writer#persist_in method which creates a
299
+ # record using Hoodoo::ActiveRecord::Creator::ClassMethods::new_in,
300
+ # then calls Hoodoo::ActiveRecord::Writer#persist_in to persist the
301
+ # data; see that for full details.
218
302
  #
219
- # Using this method for data storage instead of plain ActiveRecord
220
- # +save+ or <tt>save!</tt> will also help your code auto-inherit any
221
- # additional future write-related enhancements in Hoodoo should they
222
- # arise, without necessarily needing service code changes.
223
- #
224
- #
225
- # == Example
226
- #
227
- # class Unique < ActiveRecord::Base
228
- # include Hoodoo::ActiveRecord::Writer
229
- # validates :unique_code, :presence => true, :uniqueness => true
230
- # end
231
- #
232
- # The migration to create the table for the Unique model _MUST_ have a
233
- # uniqueness constraint on the +unique_code+ field, e.g.:
234
- #
235
- # def change
236
- # add_column :uniques, :unique_code, :null => false
237
- # add_index :uniques, [ :unique_code ], :unique => true
238
- # end
239
- #
240
- # Then, inside the implementation class which uses the above model,
241
- # where you have (say) written private methods +mapping_of+ which
242
- # maps +context.request.body+ to an attributes Hash for persistence
243
- # and +rendering_of+ which uses Hoodoo::Presenters::Base.render_in to
244
- # properly render a representation of your resource, you would write:
303
+ # As a class-based method, the return value and error handling
304
+ # semantics differ from the instance-based counterpart. Instead of
305
+ # checking the return value of +persist_in+ for success or failure,
306
+ # use ActiveRecord's "persisted?":
245
307
  #
246
308
  # def create( context )
247
309
  # attributes = mapping_of( context.request.body )
@@ -276,40 +338,8 @@ module Hoodoo
276
338
  # See also the Hoodoo::ActiveRecord::Writer#persist_in instance method
277
339
  # equivalent of this class method.
278
340
  #
279
- #
280
- # == Nested transaction note
281
- #
282
- # Ordinarily an exception in a nested transaction does not roll back.
283
- # ActiveRecord wraps all saves in a transaction "out of the box", so
284
- # the following construct could have unexpected results...
285
- #
286
- # Model.transaction do
287
- # instance.persist_in( context )
288
- # end
289
- #
290
- # ...if <tt>instance.valid?</tt> runs any SQL queries - which is very
291
- # likely. PostgreSQL, for example, would then raise an exception; the
292
- # inner transaction failed, leaving the outer one in an aborted state:
293
- #
294
- # PG::InFailedSqlTransaction: ERROR: current transaction is
295
- # aborted, commands ignored until end of transaction block
296
- #
297
- # ActiveRecord provides us with a way to define a transaction that
298
- # does roll back via the <tt>requires_new: true</tt> option. Hoodoo
299
- # thus protects callers from the above artefacts by ensuring that all
300
- # saves are wrapped in an outer transaction that causes rollback in
301
- # any parents. This sidesteps the unexpected behaviour, but service
302
- # authors might sometimes need to be aware of this if using complex
303
- # transaction behaviour along with <tt>persist_in</tt>.
304
- #
305
- # In pseudocode, the internal implementation is:
306
- #
307
- # self.transaction( :requires_new => true ) do
308
- # self.save
309
- # end
310
- #
311
341
  def persist_in( context, attributes )
312
- instance = self.new( attributes )
342
+ instance = self.new_in( context, attributes )
313
343
  instance.persist_in( context )
314
344
 
315
345
  return instance