familia 2.0.0.pre21 → 2.0.0.pre23
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 +4 -4
- data/.github/workflows/claude-code-review.yml +8 -5
- data/.talismanrc +5 -1
- data/CHANGELOG.rst +76 -0
- data/Gemfile.lock +8 -8
- data/docs/1106-participates_in-bidirectional-solution.md +201 -58
- data/examples/through_relationships.rb +275 -0
- data/lib/familia/connection/operation_core.rb +1 -2
- data/lib/familia/connection/pipelined_core.rb +1 -3
- data/lib/familia/connection/transaction_core.rb +1 -2
- data/lib/familia/data_type/serialization.rb +76 -51
- data/lib/familia/data_type/types/sorted_set.rb +5 -10
- data/lib/familia/data_type/types/stringkey.rb +22 -0
- data/lib/familia/features/external_identifier.rb +29 -0
- data/lib/familia/features/object_identifier.rb +47 -0
- data/lib/familia/features/relationships/README.md +1 -1
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +15 -15
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +8 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +59 -10
- data/lib/familia/features/relationships/participation/target_methods.rb +51 -7
- data/lib/familia/features/relationships/participation/through_model_operations.rb +150 -0
- data/lib/familia/features/relationships/participation.rb +39 -15
- data/lib/familia/features/relationships/participation_relationship.rb +19 -1
- data/lib/familia/features/relationships.rb +1 -1
- data/lib/familia/horreum/database_commands.rb +6 -1
- data/lib/familia/horreum/management.rb +141 -10
- data/lib/familia/horreum/persistence.rb +3 -0
- data/lib/familia/identifier_extractor.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/lib/multi_result.rb +59 -31
- data/pr_agent.toml +6 -1
- data/try/features/count_any_edge_cases_try.rb +486 -0
- data/try/features/count_any_methods_try.rb +197 -0
- data/try/features/external_identifier/external_identifier_try.rb +134 -0
- data/try/features/object_identifier/object_identifier_try.rb +138 -0
- data/try/features/relationships/indexing_rebuild_try.rb +6 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +1 -1
- data/try/features/relationships/participation_method_prefix_try.rb +133 -0
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/{participation_bidirectional_try.rb → participation_reverse_methods_try.rb} +6 -6
- data/try/features/relationships/participation_through_try.rb +173 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +5 -3
- data/try/integration/data_types/datatype_transactions_try.rb +13 -7
- data/try/integration/models/customer_try.rb +3 -3
- data/try/unit/data_types/boolean_try.rb +35 -22
- data/try/unit/data_types/hash_try.rb +2 -2
- data/try/unit/data_types/serialization_try.rb +386 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +2 -1
- metadata +9 -8
- data/changelog.d/20251105_flexible_external_identifier_format.rst +0 -66
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +0 -44
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +0 -20
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +0 -91
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +0 -94
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +0 -44
|
@@ -387,3 +387,137 @@ found_no_prefix&.id
|
|
|
387
387
|
findable_custom.destroy! rescue nil
|
|
388
388
|
findable_hyphen.destroy! rescue nil
|
|
389
389
|
findable_no_prefix.destroy! rescue nil
|
|
390
|
+
|
|
391
|
+
# ========================================
|
|
392
|
+
# extid? Method Test Coverage
|
|
393
|
+
# ========================================
|
|
394
|
+
|
|
395
|
+
## extid? returns false for nil
|
|
396
|
+
ExternalIdTest.extid?(nil)
|
|
397
|
+
#=> false
|
|
398
|
+
|
|
399
|
+
## extid? returns false for empty string
|
|
400
|
+
ExternalIdTest.extid?('')
|
|
401
|
+
#=> false
|
|
402
|
+
|
|
403
|
+
## extid? returns false for whitespace-only string
|
|
404
|
+
ExternalIdTest.extid?(' ')
|
|
405
|
+
#=> false
|
|
406
|
+
|
|
407
|
+
## extid? returns true for valid extid with default format
|
|
408
|
+
# Valid format: ext_ prefix + exactly 25 alphanumeric characters
|
|
409
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghijklmno')
|
|
410
|
+
#=> true
|
|
411
|
+
|
|
412
|
+
## extid? returns true for valid extid with uppercase characters
|
|
413
|
+
# The pattern allows [0-9a-zA-Z] for the ID part
|
|
414
|
+
ExternalIdTest.extid?('ext_0123456789ABCDEFGHIJKLMNO')
|
|
415
|
+
#=> true
|
|
416
|
+
|
|
417
|
+
## extid? returns true for valid extid with mixed case
|
|
418
|
+
ExternalIdTest.extid?('ext_0123456789AbCdEfGhIjKlMnO')
|
|
419
|
+
#=> true
|
|
420
|
+
|
|
421
|
+
## extid? returns true for generated extid
|
|
422
|
+
obj = ExternalIdTest.new
|
|
423
|
+
ExternalIdTest.extid?(obj.extid)
|
|
424
|
+
#=> true
|
|
425
|
+
|
|
426
|
+
## extid? returns false for wrong prefix
|
|
427
|
+
ExternalIdTest.extid?('xxx_0123456789abcdefghijklmno')
|
|
428
|
+
#=> false
|
|
429
|
+
|
|
430
|
+
## extid? returns false for missing prefix
|
|
431
|
+
ExternalIdTest.extid?('0123456789abcdefghijklmno')
|
|
432
|
+
#=> false
|
|
433
|
+
|
|
434
|
+
## extid? accepts ID part with 24 chars (within 20-32 range)
|
|
435
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghijklmn')
|
|
436
|
+
#=> true
|
|
437
|
+
|
|
438
|
+
## extid? accepts ID part with 26 chars (within 20-32 range)
|
|
439
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghijklmnop')
|
|
440
|
+
#=> true
|
|
441
|
+
|
|
442
|
+
## extid? returns false for ID part too short (19 chars, below minimum)
|
|
443
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghi')
|
|
444
|
+
#=> false
|
|
445
|
+
|
|
446
|
+
## extid? returns false for ID part too long (33 chars, above maximum)
|
|
447
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghijklmnopqrstuvw')
|
|
448
|
+
#=> false
|
|
449
|
+
|
|
450
|
+
## extid? returns false for invalid characters in ID (underscore)
|
|
451
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghij_lmno')
|
|
452
|
+
#=> false
|
|
453
|
+
|
|
454
|
+
## extid? returns false for invalid characters in ID (hyphen)
|
|
455
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghij-lmno')
|
|
456
|
+
#=> false
|
|
457
|
+
|
|
458
|
+
## extid? returns false for invalid characters in ID (special chars)
|
|
459
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghij!@#$%')
|
|
460
|
+
#=> false
|
|
461
|
+
|
|
462
|
+
## extid? with custom prefix format returns true for valid extid
|
|
463
|
+
CustomPrefixTest.extid?('cust_0123456789abcdefghijklmno')
|
|
464
|
+
#=> true
|
|
465
|
+
|
|
466
|
+
## extid? with custom prefix format returns false for default prefix
|
|
467
|
+
CustomPrefixTest.extid?('ext_0123456789abcdefghijklmno')
|
|
468
|
+
#=> false
|
|
469
|
+
|
|
470
|
+
## extid? with custom prefix format returns true for generated extid
|
|
471
|
+
custom_obj = CustomPrefixTest.new
|
|
472
|
+
CustomPrefixTest.extid?(custom_obj.extid)
|
|
473
|
+
#=> true
|
|
474
|
+
|
|
475
|
+
## extid? with hyphen format returns true for valid extid
|
|
476
|
+
CustomFormatTest.extid?('ext-0123456789abcdefghijklmno')
|
|
477
|
+
#=> true
|
|
478
|
+
|
|
479
|
+
## extid? with hyphen format returns false for underscore format
|
|
480
|
+
CustomFormatTest.extid?('ext_0123456789abcdefghijklmno')
|
|
481
|
+
#=> false
|
|
482
|
+
|
|
483
|
+
## extid? with hyphen format returns true for generated extid
|
|
484
|
+
hyphen_obj = CustomFormatTest.new
|
|
485
|
+
CustomFormatTest.extid?(hyphen_obj.extid)
|
|
486
|
+
#=> true
|
|
487
|
+
|
|
488
|
+
## extid? with no-prefix format returns true for valid extid
|
|
489
|
+
NoPrefixFormatTest.extid?('api/0123456789abcdefghijklmno')
|
|
490
|
+
#=> true
|
|
491
|
+
|
|
492
|
+
## extid? with no-prefix format returns false for default format
|
|
493
|
+
NoPrefixFormatTest.extid?('ext_0123456789abcdefghijklmno')
|
|
494
|
+
#=> false
|
|
495
|
+
|
|
496
|
+
## extid? with no-prefix format returns true for generated extid
|
|
497
|
+
no_prefix_obj = NoPrefixFormatTest.new
|
|
498
|
+
NoPrefixFormatTest.extid?(no_prefix_obj.extid)
|
|
499
|
+
#=> true
|
|
500
|
+
|
|
501
|
+
## extid? returns false for partial match at start
|
|
502
|
+
ExternalIdTest.extid?('ext_0123456789abcdefghijklmno_extra')
|
|
503
|
+
#=> false
|
|
504
|
+
|
|
505
|
+
## extid? returns false for partial match with leading chars
|
|
506
|
+
ExternalIdTest.extid?('prefix_ext_0123456789abcdefghijklmno')
|
|
507
|
+
#=> false
|
|
508
|
+
|
|
509
|
+
## extid? returns false for spaces in ID
|
|
510
|
+
ExternalIdTest.extid?('ext_0123456789 bcdefghijklmno')
|
|
511
|
+
#=> false
|
|
512
|
+
|
|
513
|
+
## extid? returns false for just the prefix
|
|
514
|
+
ExternalIdTest.extid?('ext_')
|
|
515
|
+
#=> false
|
|
516
|
+
|
|
517
|
+
## extid? with Symbol input (symbols support =~ matching)
|
|
518
|
+
ExternalIdTest.extid?(:ext_0123456789abcdefghijklmno)
|
|
519
|
+
#=> true
|
|
520
|
+
|
|
521
|
+
## extid? with Symbol input returns false for invalid format
|
|
522
|
+
ExternalIdTest.extid?(:invalid_symbol)
|
|
523
|
+
#=> false
|
|
@@ -201,3 +201,141 @@ race_obj.save # Save so find_by_objid can locate
|
|
|
201
201
|
found = BasicObjectTest.find_by_objid(generated_objid)
|
|
202
202
|
found && found.id == "race_test_123"
|
|
203
203
|
#=> true
|
|
204
|
+
|
|
205
|
+
# ========================================
|
|
206
|
+
# objid? Method Test Coverage
|
|
207
|
+
# ========================================
|
|
208
|
+
|
|
209
|
+
## objid? returns false for nil
|
|
210
|
+
BasicObjectTest.objid?(nil)
|
|
211
|
+
#=> false
|
|
212
|
+
|
|
213
|
+
## objid? returns false for empty string
|
|
214
|
+
BasicObjectTest.objid?('')
|
|
215
|
+
#=> false
|
|
216
|
+
|
|
217
|
+
## objid? returns false for whitespace-only string
|
|
218
|
+
BasicObjectTest.objid?(' ')
|
|
219
|
+
#=> false
|
|
220
|
+
|
|
221
|
+
## objid? returns true for valid UUID v7 format
|
|
222
|
+
# UUID v7 format: xxxxxxxx-xxxx-7xxx-xxxx-xxxxxxxxxxxx (version char is '7')
|
|
223
|
+
BasicObjectTest.objid?('01234567-89ab-7def-89ab-0123456789ab')
|
|
224
|
+
#=> true
|
|
225
|
+
|
|
226
|
+
## objid? returns true for generated UUID v7 objid
|
|
227
|
+
obj = BasicObjectTest.new
|
|
228
|
+
BasicObjectTest.objid?(obj.objid)
|
|
229
|
+
#=> true
|
|
230
|
+
|
|
231
|
+
## objid? returns false for UUID v4 in UUID v7 class
|
|
232
|
+
# Version char is '4' instead of '7'
|
|
233
|
+
BasicObjectTest.objid?('01234567-89ab-4def-89ab-0123456789ab')
|
|
234
|
+
#=> false
|
|
235
|
+
|
|
236
|
+
## objid? returns false for wrong version char
|
|
237
|
+
BasicObjectTest.objid?('01234567-89ab-5def-89ab-0123456789ab')
|
|
238
|
+
#=> false
|
|
239
|
+
|
|
240
|
+
## objid? returns false for missing hyphens
|
|
241
|
+
BasicObjectTest.objid?('0123456789ab7def89ab0123456789ab')
|
|
242
|
+
#=> false
|
|
243
|
+
|
|
244
|
+
## objid? returns false for wrong length (too short)
|
|
245
|
+
BasicObjectTest.objid?('01234567-89ab-7def-89ab-012345678')
|
|
246
|
+
#=> false
|
|
247
|
+
|
|
248
|
+
## objid? returns false for wrong length (too long)
|
|
249
|
+
BasicObjectTest.objid?('01234567-89ab-7def-89ab-0123456789abcd')
|
|
250
|
+
#=> false
|
|
251
|
+
|
|
252
|
+
## objid? returns false for hyphens in wrong positions
|
|
253
|
+
BasicObjectTest.objid?('0123456-789ab-7def-89ab-0123456789ab')
|
|
254
|
+
#=> false
|
|
255
|
+
|
|
256
|
+
## objid? returns false for UUID with non-hex characters
|
|
257
|
+
# Validates that all characters are valid hexadecimal digits
|
|
258
|
+
BasicObjectTest.objid?('gggggggg-gggg-7ggg-gggg-gggggggggggg')
|
|
259
|
+
#=> false
|
|
260
|
+
|
|
261
|
+
## objid? with UUID v4 generator returns true for valid v4 format
|
|
262
|
+
UuidV4Test.objid?('01234567-89ab-4def-89ab-0123456789ab')
|
|
263
|
+
#=> true
|
|
264
|
+
|
|
265
|
+
## objid? with UUID v4 generator returns true for generated objid
|
|
266
|
+
v4_obj = UuidV4Test.new
|
|
267
|
+
UuidV4Test.objid?(v4_obj.objid)
|
|
268
|
+
#=> true
|
|
269
|
+
|
|
270
|
+
## objid? with UUID v4 generator returns false for v7 format
|
|
271
|
+
UuidV4Test.objid?('01234567-89ab-7def-89ab-0123456789ab')
|
|
272
|
+
#=> false
|
|
273
|
+
|
|
274
|
+
## objid? with hex generator returns true for valid hex string
|
|
275
|
+
HexTest.objid?('0123456789abcdef')
|
|
276
|
+
#=> true
|
|
277
|
+
|
|
278
|
+
## objid? with hex generator returns true for generated hex objid
|
|
279
|
+
hex_obj = HexTest.new
|
|
280
|
+
HexTest.objid?(hex_obj.objid)
|
|
281
|
+
#=> true
|
|
282
|
+
|
|
283
|
+
## objid? with hex generator returns true for uppercase hex
|
|
284
|
+
HexTest.objid?('0123456789ABCDEF')
|
|
285
|
+
#=> true
|
|
286
|
+
|
|
287
|
+
## objid? with hex generator returns true for mixed case hex
|
|
288
|
+
HexTest.objid?('0123456789AbCdEf')
|
|
289
|
+
#=> true
|
|
290
|
+
|
|
291
|
+
## objid? with hex generator returns false for non-hex chars
|
|
292
|
+
HexTest.objid?('0123456789ghijkl')
|
|
293
|
+
#=> false
|
|
294
|
+
|
|
295
|
+
## objid? with hex generator returns false for UUID format
|
|
296
|
+
HexTest.objid?('01234567-89ab-7def-89ab-0123456789ab')
|
|
297
|
+
#=> false
|
|
298
|
+
|
|
299
|
+
## objid? with hex generator returns true for short hex
|
|
300
|
+
HexTest.objid?('abc')
|
|
301
|
+
#=> true
|
|
302
|
+
|
|
303
|
+
## objid? with hex generator returns true for long hex
|
|
304
|
+
HexTest.objid?('0123456789abcdef0123456789abcdef')
|
|
305
|
+
#=> true
|
|
306
|
+
|
|
307
|
+
## objid? with custom proc generator returns false (unsupported)
|
|
308
|
+
CustomProcTest.objid?('custom_12345678')
|
|
309
|
+
#=> false
|
|
310
|
+
|
|
311
|
+
## objid? with custom proc generator returns false for any input
|
|
312
|
+
CustomProcTest.objid?('anything')
|
|
313
|
+
#=> false
|
|
314
|
+
|
|
315
|
+
## objid? UUID validation checks position 8 for hyphen
|
|
316
|
+
BasicObjectTest.objid?('01234567X89ab-7def-89ab-0123456789ab')
|
|
317
|
+
#=> false
|
|
318
|
+
|
|
319
|
+
## objid? UUID validation checks position 13 for hyphen
|
|
320
|
+
BasicObjectTest.objid?('01234567-89abX7def-89ab-0123456789ab')
|
|
321
|
+
#=> false
|
|
322
|
+
|
|
323
|
+
## objid? UUID validation checks position 18 for hyphen
|
|
324
|
+
BasicObjectTest.objid?('01234567-89ab-7defX89ab-0123456789ab')
|
|
325
|
+
#=> false
|
|
326
|
+
|
|
327
|
+
## objid? UUID validation checks position 23 for hyphen
|
|
328
|
+
BasicObjectTest.objid?('01234567-89ab-7def-89abX0123456789ab')
|
|
329
|
+
#=> false
|
|
330
|
+
|
|
331
|
+
## objid? returns false for partial UUID match
|
|
332
|
+
BasicObjectTest.objid?('01234567-89ab-7def')
|
|
333
|
+
#=> false
|
|
334
|
+
|
|
335
|
+
## objid? with Symbol input for UUID v7 (symbols support string comparison)
|
|
336
|
+
BasicObjectTest.objid?(:'01234567-89ab-7def-89ab-0123456789ab')
|
|
337
|
+
#=> true
|
|
338
|
+
|
|
339
|
+
## objid? with Symbol input returns false for invalid format
|
|
340
|
+
BasicObjectTest.objid?(:invalid_objid)
|
|
341
|
+
#=> false
|
|
@@ -553,6 +553,9 @@ found3 = @company.find_by_badge_number("BADGE003")
|
|
|
553
553
|
# The architecture prevents this via factory pattern, but guard provides explicit protection
|
|
554
554
|
begin
|
|
555
555
|
# Simulate calling rebuild_via_participation with multi-index cardinality
|
|
556
|
+
# Note: dept_index is a multi-index, so we need to use dept_index_for to get a DataType instance
|
|
557
|
+
# But for this test, we just need any HashKey-like object to pass for serialization
|
|
558
|
+
index_hashkey = @company.badge_index # Use a valid HashKey index
|
|
556
559
|
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
|
|
557
560
|
@company,
|
|
558
561
|
RebuildTestEmployee,
|
|
@@ -560,6 +563,7 @@ begin
|
|
|
560
563
|
:add_to_rebuild_test_company_dept_index,
|
|
561
564
|
@company.employees,
|
|
562
565
|
:multi, # Wrong cardinality!
|
|
566
|
+
index_hashkey, # Added required parameter
|
|
563
567
|
batch_size: 100
|
|
564
568
|
)
|
|
565
569
|
"should have raised"
|
|
@@ -571,6 +575,7 @@ end
|
|
|
571
575
|
## Guard accepts correct cardinality (:unique)
|
|
572
576
|
begin
|
|
573
577
|
index_config = RebuildTestEmployee.indexing_relationships.find { |r| r.index_name == :badge_index }
|
|
578
|
+
index_hashkey = @company.badge_index # Get the index HashKey for serialization
|
|
574
579
|
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
|
|
575
580
|
@company,
|
|
576
581
|
RebuildTestEmployee,
|
|
@@ -578,6 +583,7 @@ begin
|
|
|
578
583
|
:add_to_rebuild_test_company_badge_index,
|
|
579
584
|
@company.employees,
|
|
580
585
|
:unique, # Correct cardinality
|
|
586
|
+
index_hashkey, # Added required parameter
|
|
581
587
|
batch_size: 100
|
|
582
588
|
)
|
|
583
589
|
"no error"
|
|
@@ -30,7 +30,7 @@ RSpec.describe 'participation_commands_verification_try' do
|
|
|
30
30
|
field :display_domain
|
|
31
31
|
field :created_at
|
|
32
32
|
participates_in ReverseIndexCustomer, :domains, score: :created_at
|
|
33
|
-
participates_in ReverseIndexCustomer, :preferred_domains,
|
|
33
|
+
participates_in ReverseIndexCustomer, :preferred_domains, generate_participant_methods: true
|
|
34
34
|
class_participates_in :all_domains, score: :created_at
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -49,7 +49,7 @@ class ReverseIndexDomain < Familia::Horreum
|
|
|
49
49
|
field :created_at
|
|
50
50
|
|
|
51
51
|
participates_in ReverseIndexCustomer, :domains, score: :created_at
|
|
52
|
-
participates_in ReverseIndexCustomer, :preferred_domains,
|
|
52
|
+
participates_in ReverseIndexCustomer, :preferred_domains, generate_participant_methods: true
|
|
53
53
|
class_participates_in :all_domains, score: :created_at
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# try/features/relationships/participation_method_prefix_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require_relative '../../../lib/familia'
|
|
6
|
+
|
|
7
|
+
# Test the method_prefix: option for participates_in
|
|
8
|
+
# This allows shorter reverse method names for namespaced classes
|
|
9
|
+
|
|
10
|
+
# Simulate a namespaced target class
|
|
11
|
+
module ::Admin
|
|
12
|
+
class MethodPrefixTeam < Familia::Horreum
|
|
13
|
+
feature :relationships
|
|
14
|
+
|
|
15
|
+
identifier_field :team_id
|
|
16
|
+
field :team_id
|
|
17
|
+
field :name
|
|
18
|
+
|
|
19
|
+
sorted_set :members
|
|
20
|
+
sorted_set :admins
|
|
21
|
+
|
|
22
|
+
def init
|
|
23
|
+
@team_id ||= "team_#{SecureRandom.hex(4)}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Test class using method_prefix: option
|
|
29
|
+
class ::MethodPrefixUser < Familia::Horreum
|
|
30
|
+
feature :relationships
|
|
31
|
+
|
|
32
|
+
identifier_field :user_id
|
|
33
|
+
field :user_id
|
|
34
|
+
field :email
|
|
35
|
+
|
|
36
|
+
# Use method_prefix to get shorter method names
|
|
37
|
+
# Instead of admin_method_prefix_team_instances, we get team_instances
|
|
38
|
+
participates_in Admin::MethodPrefixTeam, :members, method_prefix: :team
|
|
39
|
+
|
|
40
|
+
def init
|
|
41
|
+
@user_id ||= "user_#{SecureRandom.hex(4)}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Test class using as: which should take precedence over method_prefix:
|
|
46
|
+
class ::MethodPrefixPriorityUser < Familia::Horreum
|
|
47
|
+
feature :relationships
|
|
48
|
+
|
|
49
|
+
identifier_field :user_id
|
|
50
|
+
field :user_id
|
|
51
|
+
field :email
|
|
52
|
+
|
|
53
|
+
# as: takes precedence over method_prefix:
|
|
54
|
+
participates_in Admin::MethodPrefixTeam, :admins, method_prefix: :team, as: :my_teams
|
|
55
|
+
|
|
56
|
+
def init
|
|
57
|
+
@user_id ||= "user_#{SecureRandom.hex(4)}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Test class without method_prefix (default behavior)
|
|
62
|
+
class ::MethodPrefixDefaultUser < Familia::Horreum
|
|
63
|
+
feature :relationships
|
|
64
|
+
|
|
65
|
+
identifier_field :user_id
|
|
66
|
+
field :user_id
|
|
67
|
+
field :email
|
|
68
|
+
|
|
69
|
+
# Default: uses config_name (method_prefix_team - demodularized, no namespace)
|
|
70
|
+
participates_in Admin::MethodPrefixTeam, :members
|
|
71
|
+
|
|
72
|
+
def init
|
|
73
|
+
@user_id ||= "user_#{SecureRandom.hex(4)}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@user = MethodPrefixUser.new(email: 'alice@example.com')
|
|
78
|
+
@priority_user = MethodPrefixPriorityUser.new(email: 'bob@example.com')
|
|
79
|
+
@default_user = MethodPrefixDefaultUser.new(email: 'charlie@example.com')
|
|
80
|
+
@team = Admin::MethodPrefixTeam.new(name: 'Engineering')
|
|
81
|
+
|
|
82
|
+
## method_prefix: generates shortened method names
|
|
83
|
+
# The method_prefix: :team option should generate team_* methods
|
|
84
|
+
@user.respond_to?(:team_instances)
|
|
85
|
+
#=> true
|
|
86
|
+
|
|
87
|
+
## method_prefix: generates team_ids method
|
|
88
|
+
@user.respond_to?(:team_ids)
|
|
89
|
+
#=> true
|
|
90
|
+
|
|
91
|
+
## method_prefix: generates team? method
|
|
92
|
+
@user.respond_to?(:team?)
|
|
93
|
+
#=> true
|
|
94
|
+
|
|
95
|
+
## method_prefix: generates team_count method
|
|
96
|
+
@user.respond_to?(:team_count)
|
|
97
|
+
#=> true
|
|
98
|
+
|
|
99
|
+
## method_prefix: does NOT generate verbose config_name methods
|
|
100
|
+
# The verbose admin_method_prefix_team_* methods should not be created
|
|
101
|
+
@user.respond_to?(:admin_method_prefix_team_instances)
|
|
102
|
+
#=> false
|
|
103
|
+
|
|
104
|
+
## as: takes precedence over method_prefix:
|
|
105
|
+
# When both as: and method_prefix: are provided, as: wins
|
|
106
|
+
@priority_user.respond_to?(:my_teams_instances)
|
|
107
|
+
#=> true
|
|
108
|
+
|
|
109
|
+
## as: takes precedence - method_prefix method NOT generated
|
|
110
|
+
@priority_user.respond_to?(:team_instances)
|
|
111
|
+
#=> false
|
|
112
|
+
|
|
113
|
+
## default behavior preserved when no method_prefix
|
|
114
|
+
# Without method_prefix:, the config_name is used (method_prefix_team)
|
|
115
|
+
# Note: config_name uses the class name without namespace
|
|
116
|
+
@default_user.respond_to?(:method_prefix_team_instances)
|
|
117
|
+
#=> true
|
|
118
|
+
|
|
119
|
+
## default behavior - no custom shortened method names
|
|
120
|
+
# The team_instances method is not generated unless method_prefix: :team is used
|
|
121
|
+
@default_user.respond_to?(:team_instances)
|
|
122
|
+
#=> false
|
|
123
|
+
|
|
124
|
+
## ParticipationRelationship stores method_prefix
|
|
125
|
+
# The relationship metadata should include the method_prefix
|
|
126
|
+
rel = MethodPrefixUser.participation_relationships.first
|
|
127
|
+
rel.method_prefix
|
|
128
|
+
#=> :team
|
|
129
|
+
|
|
130
|
+
## ParticipationRelationship method_prefix is nil for default
|
|
131
|
+
rel_default = MethodPrefixDefaultUser.participation_relationships.first
|
|
132
|
+
rel_default.method_prefix
|
|
133
|
+
#=> nil
|
|
@@ -28,7 +28,7 @@ class ReverseIndexDomain < Familia::Horreum
|
|
|
28
28
|
field :created_at
|
|
29
29
|
|
|
30
30
|
participates_in ReverseIndexCustomer, :domains, score: :created_at
|
|
31
|
-
participates_in ReverseIndexCustomer, :preferred_domains,
|
|
31
|
+
participates_in ReverseIndexCustomer, :preferred_domains, generate_participant_methods: true
|
|
32
32
|
class_participates_in :all_domains, score: :created_at
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# try/features/relationships/
|
|
1
|
+
# try/features/relationships/participation_reverse_methods_try.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require_relative '../../../lib/familia'
|
|
6
6
|
|
|
7
|
-
# Test demonstrating
|
|
7
|
+
# Test demonstrating reverse collection methods (generate_participant_methods: true)
|
|
8
8
|
# This shows the improvement from asymmetric to symmetric relationship access
|
|
9
9
|
|
|
10
10
|
# Setup test classes
|
|
@@ -51,8 +51,8 @@ class ::ProjectUser < Familia::Horreum
|
|
|
51
51
|
field :name
|
|
52
52
|
field :role
|
|
53
53
|
|
|
54
|
-
# Define
|
|
55
|
-
# These
|
|
54
|
+
# Define participation relationships with reverse collection methods
|
|
55
|
+
# These auto-generate methods like user.project_team_instances (when generate_participant_methods: true)
|
|
56
56
|
participates_in ProjectTeam, :members # Generates: user.project_team_instances
|
|
57
57
|
participates_in ProjectTeam, :admins # Also adds to user.project_team_instances (union)
|
|
58
58
|
participates_in ProjectOrganization, :employees # Generates: user.project_organization_instances
|
|
@@ -197,13 +197,13 @@ contracting_orgs_instances.map(&:name)
|
|
|
197
197
|
@team1.admins.to_a
|
|
198
198
|
#=> [@user1.identifier]
|
|
199
199
|
|
|
200
|
-
## Test
|
|
200
|
+
## Test symmetric consistency - forward direction
|
|
201
201
|
team1_member_ids = @team1.members.to_a
|
|
202
202
|
team1_members = ProjectUser.load_multi(team1_member_ids).compact
|
|
203
203
|
team1_members.map(&:name)
|
|
204
204
|
#=> ["Alice"]
|
|
205
205
|
|
|
206
|
-
## Test
|
|
206
|
+
## Test symmetric consistency - reverse direction
|
|
207
207
|
user1_teams = @user1.project_team_instances
|
|
208
208
|
user1_team_ids = user1_teams.map(&:identifier)
|
|
209
209
|
user1_team_ids.include?(@team1.identifier)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# try/features/relationships/participation_through_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Test through model support for participates_in relationships
|
|
6
|
+
|
|
7
|
+
require_relative '../../support/helpers/test_helpers'
|
|
8
|
+
|
|
9
|
+
# Define through model FIRST so it can be resolved
|
|
10
|
+
class ::ThroughTestMembership < Familia::Horreum
|
|
11
|
+
feature :object_identifier
|
|
12
|
+
feature :relationships
|
|
13
|
+
identifier_field :objid
|
|
14
|
+
field :through_test_customer_objid
|
|
15
|
+
field :through_test_domain_objid
|
|
16
|
+
field :role
|
|
17
|
+
field :updated_at
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class ::ThroughTestCustomer < Familia::Horreum
|
|
21
|
+
feature :object_identifier
|
|
22
|
+
feature :relationships
|
|
23
|
+
identifier_field :custid
|
|
24
|
+
field :custid
|
|
25
|
+
field :name
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class ::ThroughTestDomain < Familia::Horreum
|
|
29
|
+
feature :object_identifier
|
|
30
|
+
feature :relationships
|
|
31
|
+
identifier_field :domain_id
|
|
32
|
+
field :domain_id
|
|
33
|
+
field :display_domain
|
|
34
|
+
field :created_at
|
|
35
|
+
participates_in ThroughTestCustomer, :domains, score: :created_at, through: :ThroughTestMembership
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Define backward compat classes in setup
|
|
39
|
+
class ::BackwardCompatCustomer < Familia::Horreum
|
|
40
|
+
feature :relationships
|
|
41
|
+
identifier_field :custid
|
|
42
|
+
field :custid
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class ::BackwardCompatDomain < Familia::Horreum
|
|
46
|
+
feature :relationships
|
|
47
|
+
identifier_field :domain_id
|
|
48
|
+
field :domain_id
|
|
49
|
+
field :created_at
|
|
50
|
+
participates_in BackwardCompatCustomer, :domains, score: :created_at
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Setup instance variables
|
|
54
|
+
@customer = ThroughTestCustomer.new(custid: 'cust_through_123', name: 'Through Test Customer')
|
|
55
|
+
@domain = ThroughTestDomain.new(domain_id: 'dom_through_456', display_domain: 'through-test.com', created_at: Familia.now.to_i)
|
|
56
|
+
@customer.save
|
|
57
|
+
@domain.save
|
|
58
|
+
|
|
59
|
+
@compat_customer = BackwardCompatCustomer.new(custid: 'compat_cust')
|
|
60
|
+
@compat_domain = BackwardCompatDomain.new(domain_id: 'compat_dom', created_at: Familia.now.to_i)
|
|
61
|
+
@compat_customer.save
|
|
62
|
+
@compat_domain.save
|
|
63
|
+
|
|
64
|
+
## ParticipationRelationship supports through parameter
|
|
65
|
+
Familia::Features::Relationships::ParticipationRelationship.members.include?(:through)
|
|
66
|
+
#=> true
|
|
67
|
+
|
|
68
|
+
## participates_in creates relationship with through parameter
|
|
69
|
+
@rel = ThroughTestDomain.participation_relationships.first
|
|
70
|
+
@rel.through
|
|
71
|
+
#=> :ThroughTestMembership
|
|
72
|
+
|
|
73
|
+
## Through class must have object_identifier feature
|
|
74
|
+
begin
|
|
75
|
+
class ::InvalidThroughModel < Familia::Horreum
|
|
76
|
+
feature :relationships
|
|
77
|
+
end
|
|
78
|
+
class ::BadDomain < Familia::Horreum
|
|
79
|
+
feature :relationships
|
|
80
|
+
field :name
|
|
81
|
+
participates_in ThroughTestCustomer, :bad_domains, through: :InvalidThroughModel
|
|
82
|
+
end
|
|
83
|
+
false
|
|
84
|
+
rescue ArgumentError => e
|
|
85
|
+
e.message.include?('must use `feature :object_identifier`')
|
|
86
|
+
end
|
|
87
|
+
#=> true
|
|
88
|
+
|
|
89
|
+
## Adding domain creates through model automatically
|
|
90
|
+
@membership1 = @customer.add_domains_instance(@domain)
|
|
91
|
+
@through_key = "through_test_customer:#{@customer.objid}:through_test_domain:#{@domain.objid}:through_test_membership"
|
|
92
|
+
@loaded_membership = ThroughTestMembership.load(@through_key)
|
|
93
|
+
@loaded_membership&.exists?
|
|
94
|
+
#=> true
|
|
95
|
+
|
|
96
|
+
## Through model receives through_attrs on add
|
|
97
|
+
@customer.remove_domains_instance(@domain)
|
|
98
|
+
@membership2 = @customer.add_domains_instance(@domain, through_attrs: { role: 'admin' })
|
|
99
|
+
@loaded_with_role = ThroughTestMembership.load(@through_key)
|
|
100
|
+
@loaded_with_role.role
|
|
101
|
+
#=> 'admin'
|
|
102
|
+
|
|
103
|
+
## add_domains_instance returns through model when using :through
|
|
104
|
+
@membership2.class.name
|
|
105
|
+
#=> "ThroughTestMembership"
|
|
106
|
+
|
|
107
|
+
## Returned through model can be chained
|
|
108
|
+
@membership2.respond_to?(:role)
|
|
109
|
+
#=> true
|
|
110
|
+
|
|
111
|
+
## Returned through model has role attribute set
|
|
112
|
+
@membership2.role
|
|
113
|
+
#=> 'admin'
|
|
114
|
+
|
|
115
|
+
## Removing domain destroys through model
|
|
116
|
+
@customer.remove_domains_instance(@domain)
|
|
117
|
+
@removed_check = ThroughTestMembership.load(@through_key)
|
|
118
|
+
@removed_check.nil? || !@removed_check.exists?
|
|
119
|
+
#=> true
|
|
120
|
+
|
|
121
|
+
## Adding twice updates existing, doesn't duplicate
|
|
122
|
+
@membership_v1 = @customer.add_domains_instance(@domain, through_attrs: { role: 'viewer' })
|
|
123
|
+
@check_v1 = ThroughTestMembership.load(@through_key)
|
|
124
|
+
@check_v1.role
|
|
125
|
+
#=> 'viewer'
|
|
126
|
+
|
|
127
|
+
## Second add updates the same through model
|
|
128
|
+
@membership_v2 = @customer.add_domains_instance(@domain, through_attrs: { role: 'editor' })
|
|
129
|
+
@check_v2 = ThroughTestMembership.load(@through_key)
|
|
130
|
+
@check_v2.role
|
|
131
|
+
#=> 'editor'
|
|
132
|
+
|
|
133
|
+
## Only one through model exists (same objid)
|
|
134
|
+
@check_v1.objid == @check_v2.objid
|
|
135
|
+
#=> true
|
|
136
|
+
|
|
137
|
+
## Models without :through work as before
|
|
138
|
+
@compat_result = @compat_customer.add_domains_instance(@compat_domain)
|
|
139
|
+
@compat_domain.in_backward_compat_customer_domains?(@compat_customer)
|
|
140
|
+
#=> true
|
|
141
|
+
|
|
142
|
+
## No through model created for backward compat
|
|
143
|
+
@compat_rel = BackwardCompatDomain.participation_relationships.first
|
|
144
|
+
@compat_rel.through.nil?
|
|
145
|
+
#=> true
|
|
146
|
+
|
|
147
|
+
## Backward compat returns self not through model
|
|
148
|
+
@compat_result.class.name
|
|
149
|
+
#=> "BackwardCompatCustomer"
|
|
150
|
+
|
|
151
|
+
## Through model sets updated_at on create
|
|
152
|
+
@customer.remove_domains_instance(@domain)
|
|
153
|
+
@membership_with_ts = @customer.add_domains_instance(@domain, through_attrs: { role: 'owner' })
|
|
154
|
+
@ts_check = ThroughTestMembership.load(@through_key)
|
|
155
|
+
@ts_check.updated_at.is_a?(Float)
|
|
156
|
+
#=> true
|
|
157
|
+
|
|
158
|
+
## updated_at is current
|
|
159
|
+
(Familia.now.to_f - @ts_check.updated_at) < 2.0
|
|
160
|
+
#=> true
|
|
161
|
+
|
|
162
|
+
## Through model updates updated_at on attribute change
|
|
163
|
+
@old_ts = @ts_check.updated_at
|
|
164
|
+
sleep 0.1
|
|
165
|
+
@membership_updated = @customer.add_domains_instance(@domain, through_attrs: { role: 'admin' })
|
|
166
|
+
@new_ts_check = ThroughTestMembership.load(@through_key)
|
|
167
|
+
@new_ts_check.updated_at > @old_ts
|
|
168
|
+
#=> true
|
|
169
|
+
|
|
170
|
+
# Cleanup
|
|
171
|
+
[@customer, @domain, @compat_customer, @compat_domain].each do |obj|
|
|
172
|
+
obj.destroy! if obj&.respond_to?(:destroy!) && obj.exists?
|
|
173
|
+
end
|