hoodoo 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|