hoodoo 1.1.0 → 1.1.1
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 +8 -8
- data/lib/hoodoo/active/active_record/dated.rb +129 -40
- data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +1 -1
- data/lib/hoodoo/version.rb +1 -1
- data/spec/active/active_record/dated_spec.rb +103 -6
- data/spec/active/active_record/support_spec.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MDliMjQ5YTcwM2ExNTU4YzgxY2U4NTNmZTM1ODFkMTI0NzY0MmMzZQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ZWY4NTg4ODEzOTliMGQyMTBjMmFkZGNmMmYzZmFjZjc0NDlmMWJlZQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
NjlkZTI4NjZhNDczZTdmZTM5ODlmNjVhNTY0MjQ1MGI3MDg4YmMxNDE3MDQw
|
10
|
+
NjMzOWY1MTE0MmM0N2E1MGYwNTEwODlmMzQ0M2I5YjEwZGY0ZWY4MTA5MGYw
|
11
|
+
NmRlZWViMWJiOGVlNDdkMTFmNjYzYjYxZTFhM2M4MzgyY2ZkYTE=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MzUzNmJlYTM4ZTc3ZDgyZTExMzU5ZjhhNzE2OTQxMWY1OWFlMWQwY2Q2NTgy
|
14
|
+
N2FiNmU3YmJkYjJhZDBlMjQ5NzNkZmJmYmIxY2JmMTZjYmQ0MDgzNmNmMGRj
|
15
|
+
YmRmNjJiMTY3NGU2OTRkNTc1ZGNkZDNkZjZmNjY5NTM1YmNiODI=
|
@@ -144,13 +144,25 @@ module Hoodoo
|
|
144
144
|
model.extend( ClassMethods )
|
145
145
|
end
|
146
146
|
|
147
|
-
#
|
148
|
-
# escaped
|
147
|
+
# Returns a String containing the specified +model_klass+'s attribute
|
148
|
+
# names considered as column names, escaped by the in-use database
|
149
|
+
# adaptor and joined into with commas.
|
149
150
|
#
|
150
|
-
# +model_klass
|
151
|
+
# +model_klass+:: Class which responds to <tt>#attribute_names</tt>.
|
151
152
|
#
|
152
153
|
def self.sanitised_column_string( model_klass )
|
153
|
-
|
154
|
+
self.sanitised_column_string_for( model_klass.attribute_names )
|
155
|
+
end
|
156
|
+
|
157
|
+
# As ::sanitised_column_string but takes the array of attribute or
|
158
|
+
# column names directly.
|
159
|
+
#
|
160
|
+
# +attribute_array+:: Array of column names, as Strings or Symbols.
|
161
|
+
#
|
162
|
+
def self.sanitised_column_string_for( attribute_array )
|
163
|
+
attribute_array.map do | c |
|
164
|
+
ActiveRecord::Base.connection.quote_column_name( c )
|
165
|
+
end.join( ',' )
|
154
166
|
end
|
155
167
|
|
156
168
|
# Collection of class methods that get defined on an including class via
|
@@ -200,9 +212,20 @@ module Hoodoo
|
|
200
212
|
# the Hoodoo::Services::Implementation instance methods
|
201
213
|
# that a resource subclass implements.
|
202
214
|
#
|
203
|
-
|
215
|
+
# Additional _named_ parameters are:
|
216
|
+
#
|
217
|
+
# +unquoted_column_names+:: (Optional) An Array of Strings giving one
|
218
|
+
# or more column names to use for the query.
|
219
|
+
# If omitted, all model attribtues are used
|
220
|
+
# as columns. If the "id" column is not
|
221
|
+
# included in the Array, it will be added
|
222
|
+
# anyway as this column is mandatory. The
|
223
|
+
# effect is equivalent to an Array given in
|
224
|
+
# the ActiveRecord +select+ method.
|
225
|
+
#
|
226
|
+
def dated( context, unquoted_column_names: nil )
|
204
227
|
date_time = context.request.dated_at || Time.now
|
205
|
-
return self.dated_at( date_time )
|
228
|
+
return self.dated_at( date_time, unquoted_column_names: unquoted_column_names )
|
206
229
|
end
|
207
230
|
|
208
231
|
# Return an ActiveRecord::Relation scoping a query to include only model
|
@@ -215,39 +238,52 @@ module Hoodoo
|
|
215
238
|
# can be converted to a DateTime instance, for which the
|
216
239
|
# "effective dated" scope is to be constructed.
|
217
240
|
#
|
218
|
-
|
241
|
+
# Additional _named_ parameters are:
|
242
|
+
#
|
243
|
+
# +unquoted_column_names+:: (Optional) An Array of Strings giving one
|
244
|
+
# or more column names to use for the query.
|
245
|
+
# If omitted, all model attribtues are used
|
246
|
+
# as columns. If the "id" column is not
|
247
|
+
# included in the Array, it will be added
|
248
|
+
# anyway as this column is mandatory. The
|
249
|
+
# effect is equivalent to an Array given in
|
250
|
+
# the ActiveRecord +select+ method.
|
251
|
+
#
|
252
|
+
def dated_at( date_time = Time.now, unquoted_column_names: nil )
|
219
253
|
|
220
254
|
dating_table_name = dated_with_table_name()
|
221
255
|
return all() if dating_table_name.nil? # "Model.all" -> returns anonymous scope
|
222
256
|
|
223
257
|
# Rationalise and convert the date time to UTC.
|
224
258
|
|
225
|
-
date_time
|
226
|
-
|
227
|
-
# Create a string that specifies this model's attributes escaped and
|
228
|
-
# joined by commas for use in a SQL query.
|
259
|
+
date_time = Hoodoo::Utilities.rationalise_datetime( date_time ).utc
|
260
|
+
safe_date_time = self.sanitize( date_time ) # ActiveRecord provides #sanitize
|
229
261
|
|
230
|
-
|
262
|
+
# Create strings that specify the required attributes escaped and
|
263
|
+
# joined by commas for use in a SQL query, for both main and history
|
264
|
+
# tables.
|
231
265
|
|
232
|
-
|
266
|
+
safe_name_string = self.quoted_column_name_string(
|
267
|
+
unquoted_column_names: unquoted_column_names
|
268
|
+
)
|
233
269
|
|
234
|
-
|
270
|
+
safe_history_name_string = self.quoted_column_name_string_for_history(
|
271
|
+
unquoted_column_names: unquoted_column_names
|
272
|
+
)
|
235
273
|
|
236
274
|
# A query that combines historical and current records which are
|
237
275
|
# effective at the specified date time.
|
238
276
|
|
239
277
|
nested_query = %{
|
240
278
|
(
|
241
|
-
SELECT #{
|
242
|
-
SELECT #{
|
279
|
+
SELECT #{ safe_name_string } FROM (
|
280
|
+
SELECT #{ safe_name_string },"updated_at" AS "effective_start",NULL AS "effective_end"
|
243
281
|
FROM #{ self.table_name }
|
244
|
-
|
245
282
|
UNION ALL
|
246
|
-
|
247
|
-
SELECT #{ self.dated_with_history_column_mapping }, effective_start, effective_end
|
283
|
+
SELECT #{ safe_history_name_string },"effective_start","effective_end"
|
248
284
|
FROM #{ dating_table_name }
|
249
285
|
) AS u
|
250
|
-
WHERE effective_start <= #{
|
286
|
+
WHERE "effective_start" <= #{ safe_date_time } AND ("effective_end" > #{ safe_date_time } OR "effective_end" IS NULL)
|
251
287
|
) AS #{ self.table_name }
|
252
288
|
}
|
253
289
|
|
@@ -263,26 +299,42 @@ module Hoodoo
|
|
263
299
|
# If historic dating hasn't been enabled via a call to #dating_enabled,
|
264
300
|
# then the default 'all' scope is returned instead.
|
265
301
|
#
|
266
|
-
|
302
|
+
# _Named_ parameters are:
|
303
|
+
#
|
304
|
+
# +unquoted_column_names+:: (Optional) An Array of Strings giving one
|
305
|
+
# or more column names to use for the query.
|
306
|
+
# If omitted, all model attribtues are used
|
307
|
+
# as columns. If the "id" column is not
|
308
|
+
# included in the Array, it will be added
|
309
|
+
# anyway as this column is mandatory. The
|
310
|
+
# effect is equivalent to an Array given in
|
311
|
+
# the ActiveRecord +select+ method.
|
312
|
+
#
|
313
|
+
def dated_historical_and_current( unquoted_column_names: nil )
|
267
314
|
|
268
315
|
dating_table_name = dated_with_table_name()
|
269
316
|
return all() if dating_table_name.nil? # "Model.all" -> returns anonymous scope
|
270
317
|
|
271
|
-
# Create
|
272
|
-
# joined by commas for use in a SQL query
|
318
|
+
# Create strings that specify the required attributes escaped and
|
319
|
+
# joined by commas for use in a SQL query, for both main and history
|
320
|
+
# tables.
|
321
|
+
|
322
|
+
safe_name_string = self.quoted_column_name_string(
|
323
|
+
unquoted_column_names: unquoted_column_names
|
324
|
+
)
|
273
325
|
|
274
|
-
|
326
|
+
safe_history_name_string = self.quoted_column_name_string_for_history(
|
327
|
+
unquoted_column_names: unquoted_column_names
|
328
|
+
)
|
275
329
|
|
276
330
|
# A query that combines historical and current records.
|
277
331
|
|
278
332
|
nested_query = %{
|
279
333
|
(
|
280
|
-
SELECT #{
|
334
|
+
SELECT #{ safe_name_string }
|
281
335
|
FROM #{ self.table_name }
|
282
|
-
|
283
336
|
UNION ALL
|
284
|
-
|
285
|
-
SELECT #{ self.dated_with_history_column_mapping }
|
337
|
+
SELECT #{ safe_history_name_string }
|
286
338
|
FROM #{ dating_table_name }
|
287
339
|
) AS #{ self.table_name }
|
288
340
|
}
|
@@ -314,27 +366,64 @@ module Hoodoo
|
|
314
366
|
instance.nil? ? nil : instance.table_name
|
315
367
|
end
|
316
368
|
|
317
|
-
|
369
|
+
protected
|
318
370
|
|
319
|
-
#
|
320
|
-
#
|
371
|
+
# Takes an Array of unquoted column names and returns a new Array of
|
372
|
+
# names quoted by the current database adapter.
|
321
373
|
#
|
322
|
-
|
323
|
-
|
374
|
+
# +unquoted_column_names+:: Optional Array of unquoted column names
|
375
|
+
# to use. Must contain only Strings.
|
376
|
+
#
|
377
|
+
def quoted_column_names( unquoted_column_names )
|
378
|
+
return unquoted_column_names.map do | c |
|
379
|
+
ActiveRecord::Base.connection.quote_column_name( c )
|
380
|
+
end
|
381
|
+
end
|
324
382
|
|
325
|
-
|
383
|
+
# Returns a String of comma-separated sanitised (quoted) column names
|
384
|
+
# based on this model's attribute names, or the given array of unquoted
|
385
|
+
# column names.
|
386
|
+
#
|
387
|
+
# _Named_ parameters are:
|
388
|
+
#
|
389
|
+
# +unquoted_column_names+:: Optional Array of unquoted column names
|
390
|
+
# to use. Must contain only Strings. If column
|
391
|
+
# "id" is missing, it will be added for you.
|
392
|
+
#
|
393
|
+
def quoted_column_name_string( unquoted_column_names: nil )
|
394
|
+
unquoted_column_names ||= self.attribute_names()
|
395
|
+
unquoted_column_names << 'id' unless unquoted_column_names.include?( 'id' )
|
326
396
|
|
327
|
-
|
397
|
+
return self.quoted_column_names( unquoted_column_names ).join( ',' )
|
398
|
+
end
|
328
399
|
|
329
|
-
|
400
|
+
# As ::quoted_column_name_string, but returns a String appropriate for
|
401
|
+
# the history table. Notably, this requires a source column of "uuid" to
|
402
|
+
# be mapped in as column name "id" and works on the assumption that the
|
403
|
+
# primary key is "id".
|
404
|
+
#
|
405
|
+
# _Named_ parameters are:
|
406
|
+
#
|
407
|
+
# +unquoted_column_names+:: Optional Array of unquoted column names
|
408
|
+
# to use. Must contain only Strings. If column
|
409
|
+
# "id" is missing, it will be added for you.
|
410
|
+
#
|
411
|
+
def quoted_column_name_string_for_history( unquoted_column_names: nil )
|
412
|
+
unquoted_column_names ||= self.attribute_names
|
413
|
+
primary_key_index = unquoted_column_names.index( 'id' )
|
330
414
|
|
331
|
-
|
415
|
+
if primary_key_index.nil?
|
416
|
+
unquoted_column_names << 'id'
|
417
|
+
primary_key_index = unquoted_column_names.count - 1
|
418
|
+
end
|
332
419
|
|
333
|
-
|
420
|
+
quoted_column_names = self.quoted_column_names( unquoted_column_names )
|
421
|
+
quoted_primary_key_name = quoted_column_names[ primary_key_index ]
|
422
|
+
history_primary_key = '"uuid" AS ' << quoted_primary_key_name
|
334
423
|
|
335
|
-
|
424
|
+
quoted_column_names[ primary_key_index ] = history_primary_key
|
336
425
|
|
337
|
-
return
|
426
|
+
return quoted_column_names.join( ',' )
|
338
427
|
end
|
339
428
|
|
340
429
|
end
|
data/lib/hoodoo/version.rb
CHANGED
@@ -77,7 +77,7 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
77
77
|
@uuid_a = Hoodoo::UUID.generate
|
78
78
|
@uuid_b = Hoodoo::UUID.generate
|
79
79
|
|
80
|
-
@now
|
80
|
+
@now = Time.now.utc
|
81
81
|
|
82
82
|
# uuid, data, created_at, effective_end, effective_start
|
83
83
|
[
|
@@ -208,7 +208,6 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
208
208
|
end
|
209
209
|
|
210
210
|
context '.dated_historical_and_current' do
|
211
|
-
|
212
211
|
it 'returns counts correctly' do
|
213
212
|
expect( model_klass.dated_historical_and_current.count ).to be 6
|
214
213
|
end
|
@@ -217,14 +216,41 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
217
216
|
expect( model_klass.dated_historical_and_current.pluck( :data ) ).to match_array( [ 'one', 'two', 'three', 'four', 'five', 'six' ] )
|
218
217
|
end
|
219
218
|
|
219
|
+
context 'SQL' do
|
220
|
+
it 'has expected default columns' do
|
221
|
+
sql = model_klass.dated_historical_and_current.to_sql.downcase
|
222
|
+
|
223
|
+
expect( sql ).to include( 'select "id","data","created_at","updated_at"' )
|
224
|
+
expect( sql ).to include( 'select "uuid" as "id","data","created_at","updated_at"' )
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'handles custom column selections' do
|
228
|
+
sql = model_klass.dated_historical_and_current(
|
229
|
+
unquoted_column_names: [ 'id', 'created_at' ]
|
230
|
+
).to_sql.downcase
|
231
|
+
|
232
|
+
expect( sql ).to include( 'select "id","created_at"' )
|
233
|
+
expect( sql ).to include( 'select "uuid" as "id","created_at"' )
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'handles custom column selections that omit "id"' do
|
237
|
+
sql = model_klass.dated_historical_and_current(
|
238
|
+
unquoted_column_names: [ 'created_at' ]
|
239
|
+
).to_sql.downcase
|
240
|
+
|
241
|
+
expect( sql ).to include( 'select "created_at","id"' )
|
242
|
+
expect( sql ).to include( 'select "created_at","uuid" as "id"' )
|
243
|
+
end
|
244
|
+
end
|
220
245
|
end
|
221
246
|
|
222
247
|
end
|
223
248
|
|
224
249
|
context "using default effective dating config" do
|
225
250
|
|
226
|
-
# Must be defined as a method rather than using a let statement as
|
227
|
-
# statement values cannot be used in before blocks.
|
251
|
+
# Must be defined as a method rather than using a 'let' statement as
|
252
|
+
# 'let' statement values cannot be used in 'before' blocks.
|
253
|
+
#
|
228
254
|
def model_klass
|
229
255
|
RSpecModelEffectiveDateTest
|
230
256
|
end
|
@@ -235,8 +261,9 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
235
261
|
|
236
262
|
context "overriding history table name" do
|
237
263
|
|
238
|
-
# Must be defined as a method rather than using a let statement as
|
239
|
-
# statement values cannot be used in before blocks.
|
264
|
+
# Must be defined as a method rather than using a 'let' statement as
|
265
|
+
# 'let' statement values cannot be used in 'before' blocks.
|
266
|
+
#
|
240
267
|
def model_klass
|
241
268
|
RSpecModelEffectiveDateTestOverride
|
242
269
|
end
|
@@ -245,4 +272,74 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
245
272
|
|
246
273
|
end
|
247
274
|
|
275
|
+
context "SQL and column selections" do
|
276
|
+
before :each do
|
277
|
+
@now = Time.now.utc
|
278
|
+
@safe_now = RSpecModelEffectiveDateTestOverride.sanitize( @now )
|
279
|
+
|
280
|
+
request = Hoodoo::Services::Request.new
|
281
|
+
@context = Hoodoo::Services::Context.new( nil, request, nil, nil )
|
282
|
+
|
283
|
+
@context.request.dated_at = @now
|
284
|
+
end
|
285
|
+
|
286
|
+
def run_other_expectations( sql )
|
287
|
+
expect( sql ).to include( "from r_spec_model_effective_date_history_entries" )
|
288
|
+
expect( sql ).to include( "\"effective_start\" <= #{ @safe_now }" )
|
289
|
+
expect( sql ).to include( "\"effective_end\" > #{ @safe_now }" )
|
290
|
+
expect( sql ).to include( "\"effective_end\" is null" )
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'generates expected basic SQL' do
|
294
|
+
sql = RSpecModelEffectiveDateTestOverride.dated( @context ).to_sql.downcase
|
295
|
+
|
296
|
+
expect( sql ).to include( 'select "id","data","created_at","updated_at"' )
|
297
|
+
expect( sql ).to include( 'select "uuid" as "id","data","created_at","updated_at"' )
|
298
|
+
run_other_expectations( sql )
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'generates expected column-selected SQL via #dated' do
|
302
|
+
sql = RSpecModelEffectiveDateTestOverride.dated(
|
303
|
+
@context,
|
304
|
+
unquoted_column_names: [ 'id', 'created_at' ]
|
305
|
+
).to_sql.downcase
|
306
|
+
|
307
|
+
expect( sql ).to include( 'select "id","created_at"' )
|
308
|
+
expect( sql ).to include( 'select "uuid" as "id","created_at"' )
|
309
|
+
run_other_expectations( sql )
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'includes "id" if omitted, via #dated' do
|
313
|
+
sql = RSpecModelEffectiveDateTestOverride.dated(
|
314
|
+
@context,
|
315
|
+
unquoted_column_names: [ 'created_at' ]
|
316
|
+
).to_sql.downcase
|
317
|
+
|
318
|
+
expect( sql ).to include( 'select "created_at","id"' )
|
319
|
+
expect( sql ).to include( 'select "created_at","uuid" as "id"' )
|
320
|
+
run_other_expectations( sql )
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'generates expected column-selected SQL via #dated_at' do
|
324
|
+
sql = RSpecModelEffectiveDateTestOverride.dated_at(
|
325
|
+
@now,
|
326
|
+
unquoted_column_names: [ 'id', 'created_at' ]
|
327
|
+
).to_sql.downcase
|
328
|
+
|
329
|
+
expect( sql ).to include( 'select "id","created_at"' )
|
330
|
+
expect( sql ).to include( 'select "uuid" as "id","created_at"' )
|
331
|
+
run_other_expectations( sql )
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'includes "id" if omitted, via #dated_at' do
|
335
|
+
sql = RSpecModelEffectiveDateTestOverride.dated_at(
|
336
|
+
@now,
|
337
|
+
unquoted_column_names: [ 'created_at' ]
|
338
|
+
).to_sql.downcase
|
339
|
+
|
340
|
+
expect( sql ).to include( 'select "created_at","id"' )
|
341
|
+
expect( sql ).to include( 'select "created_at","uuid" as "id"' )
|
342
|
+
run_other_expectations( sql )
|
343
|
+
end
|
344
|
+
end
|
248
345
|
end
|
@@ -163,7 +163,7 @@ describe Hoodoo::ActiveRecord::Support do
|
|
163
163
|
manual_scope = RSpecFullScopeForTestSubclass.dated( @context ).to_sql()
|
164
164
|
|
165
165
|
expect( manual_scope ).to include( "FROM #{ @thtname1 }" )
|
166
|
-
expect( manual_scope ).to include( "effective_end > #{ RSpecFullScopeForTestSubclass.sanitize( @test_time_value ) }" )
|
166
|
+
expect( manual_scope ).to include( "\"effective_end\" > #{ RSpecFullScopeForTestSubclass.sanitize( @test_time_value ) }" )
|
167
167
|
end
|
168
168
|
|
169
169
|
it 'secure' do
|
@@ -201,7 +201,7 @@ describe Hoodoo::ActiveRecord::Support do
|
|
201
201
|
manual_scope = RSpecFullScopeForTestBaseSubclassWithoutOverrides.dated( @context ).to_sql()
|
202
202
|
|
203
203
|
expect( manual_scope ).to include( "FROM #{ @thtname2 }" )
|
204
|
-
expect( manual_scope ).to include( "effective_end > #{ RSpecFullScopeForTestBaseSubclassWithoutOverrides.sanitize( @test_time_value ) }" )
|
204
|
+
expect( manual_scope ).to include( "\"effective_end\" > #{ RSpecFullScopeForTestBaseSubclassWithoutOverrides.sanitize( @test_time_value ) }" )
|
205
205
|
end
|
206
206
|
|
207
207
|
it 'secure' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hoodoo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Loyalty New Zealand
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uuidtools
|