familia 2.0.0.pre22 → 2.0.0.pre24

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.
@@ -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, bidirectional: true
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, bidirectional: true
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, bidirectional: true
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/participation_bidirectional_try.rb
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 true bidirectional participation functionality
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 bidirectional participation relationships
55
- # These will auto-generate reverse collection methods with _instances suffix
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 bidirectional consistency - forward direction
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 bidirectional consistency - reverse direction
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: familia
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre22
4
+ version: 2.0.0.pre24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -218,6 +218,7 @@ files:
218
218
  - examples/safe_dump.rb
219
219
  - examples/sampling_demo.rb
220
220
  - examples/single_connection_transaction_confusions.rb
221
+ - examples/through_relationships.rb
221
222
  - familia.gemspec
222
223
  - lib/familia.rb
223
224
  - lib/familia/base.rb
@@ -275,6 +276,7 @@ files:
275
276
  - lib/familia/features/relationships/participation/participant_methods.rb
276
277
  - lib/familia/features/relationships/participation/rebuild_strategies.md
277
278
  - lib/familia/features/relationships/participation/target_methods.rb
279
+ - lib/familia/features/relationships/participation/through_model_operations.rb
278
280
  - lib/familia/features/relationships/participation_membership.rb
279
281
  - lib/familia/features/relationships/participation_relationship.rb
280
282
  - lib/familia/features/relationships/score_encoding.rb
@@ -315,6 +317,7 @@ files:
315
317
  - pr_agent.toml
316
318
  - pr_compliance_checklist.yaml
317
319
  - try/edge_cases/empty_identifiers_try.rb
320
+ - try/edge_cases/find_by_dbkey_race_condition_try.rb
318
321
  - try/edge_cases/hash_symbolization_try.rb
319
322
  - try/edge_cases/json_serialization_try.rb
320
323
  - try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb
@@ -361,12 +364,14 @@ files:
361
364
  - try/features/relationships/indexing_commands_verification_try.rb
362
365
  - try/features/relationships/indexing_rebuild_try.rb
363
366
  - try/features/relationships/indexing_try.rb
364
- - try/features/relationships/participation_bidirectional_try.rb
365
367
  - try/features/relationships/participation_commands_verification_spec.rb
366
368
  - try/features/relationships/participation_commands_verification_try.rb
369
+ - try/features/relationships/participation_method_prefix_try.rb
367
370
  - try/features/relationships/participation_performance_improvements_try.rb
368
371
  - try/features/relationships/participation_reverse_index_try.rb
372
+ - try/features/relationships/participation_reverse_methods_try.rb
369
373
  - try/features/relationships/participation_target_class_resolution_try.rb
374
+ - try/features/relationships/participation_through_try.rb
370
375
  - try/features/relationships/participation_unresolved_target_try.rb
371
376
  - try/features/relationships/relationships_api_changes_try.rb
372
377
  - try/features/relationships/relationships_edge_cases_try.rb