familia 2.0.0.pre7 → 2.0.0.pre10

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -0
  3. data/.github/workflows/docs.yml +1 -1
  4. data/.gitignore +9 -9
  5. data/.rubocop.yml +19 -0
  6. data/.yardopts +22 -1
  7. data/CHANGELOG.md +184 -0
  8. data/CLAUDE.md +8 -5
  9. data/Gemfile +1 -1
  10. data/Gemfile.lock +3 -3
  11. data/README.md +97 -2
  12. data/changelog.d/README.md +66 -0
  13. data/changelog.d/fragments/.keep +0 -0
  14. data/changelog.d/template.md.j2 +29 -0
  15. data/docs/archive/.gitignore +2 -0
  16. data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
  17. data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
  18. data/docs/archive/FAMILIA_UPDATE.md +226 -0
  19. data/docs/archive/README.md +67 -0
  20. data/docs/guides/.gitignore +2 -0
  21. data/docs/{wiki → guides}/Feature-System-Guide.md +0 -15
  22. data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
  23. data/docs/guides/relationships-methods.md +266 -0
  24. data/examples/relationships_basic.rb +90 -157
  25. data/familia.gemspec +4 -4
  26. data/lib/familia/connection.rb +4 -21
  27. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
  28. data/lib/familia/features/external_identifiers.rb +111 -0
  29. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
  30. data/lib/familia/features/object_identifiers.rb +194 -0
  31. data/lib/familia/features/relationships/cascading.rb +0 -1
  32. data/lib/familia/features/relationships/indexing.rb +160 -176
  33. data/lib/familia/features/relationships/membership.rb +16 -22
  34. data/lib/familia/features/relationships/querying.rb +7 -12
  35. data/lib/familia/features/relationships/score_encoding.rb +1 -3
  36. data/lib/familia/features/relationships/tracking.rb +61 -22
  37. data/lib/familia/features/relationships.rb +15 -8
  38. data/lib/familia/features/transient_fields.rb +8 -10
  39. data/lib/familia/features.rb +16 -13
  40. data/lib/familia/horreum/core/serialization.rb +2 -5
  41. data/lib/familia/horreum/subclass/definition.rb +36 -0
  42. data/lib/familia/horreum.rb +15 -24
  43. data/lib/familia/version.rb +1 -3
  44. data/setup.cfg +12 -0
  45. data/try/core/errors_try.rb +1 -1
  46. data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
  47. data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
  48. data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
  49. data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
  50. data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
  51. data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
  52. data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
  53. data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
  54. data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
  55. data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
  56. data/try/features/relationships/relationships_api_changes_try.rb +339 -0
  57. data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
  58. data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
  59. data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
  60. data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
  61. data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
  62. data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +7 -6
  63. data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
  64. data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
  65. data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
  66. data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
  67. metadata +80 -60
  68. /data/docs/{wiki → guides}/API-Reference.md +0 -0
  69. /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
  70. /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
  71. /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
  72. /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
  73. /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
  74. /data/docs/{wiki → guides}/Home.md +0 -0
  75. /data/docs/{wiki → guides}/Implementation-Guide.md +0 -0
  76. /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
  77. /data/docs/{wiki → guides}/Security-Model.md +0 -0
  78. /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
  79. /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
  80. /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
  81. /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
  82. /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
  83. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
  84. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
  85. /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
  86. /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
  87. /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
  88. /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
  89. /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
  90. /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
  91. /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
@@ -17,21 +17,25 @@ module Familia
17
17
  # Define an indexed_by relationship for fast lookups
18
18
  #
19
19
  # @param field [Symbol] The field to index on
20
- # @param context [Class, Symbol] The context class that owns the index
21
20
  # @param index_name [Symbol] Name of the index hash
21
+ # @param parent [Class, Symbol] The parent class that owns the index
22
22
  # @param finder [Boolean] Whether to generate finder methods
23
23
  #
24
24
  # @example Basic indexing
25
- # indexed_by :display_name, context: Customer, index_name: :domain_index
25
+ # indexed_by :display_name, parent: Customer, index_name: :domain_index
26
26
  #
27
- # @example Global indexing
28
- # indexed_by :domain_id, context: :global, index_name: :domain_lookup
29
- def indexed_by(field, index_name, context:, finder: true)
30
- context_class = context == :global ? :global : context
31
- context_class_name = if context_class == :global
32
- 'global'
27
+ # @example Parent-based indexing
28
+ # indexed_by :user_id, :user_memberships, parent: User
29
+ def indexed_by(field, index_name, parent:, finder: true)
30
+ context_class = parent
31
+ context_class_name = if context_class.is_a?(Class)
32
+ # Extract just the class name without module prefixes or object representations
33
+ class_name = context_class.name
34
+ class_name = class_name.split('::').last if class_name
35
+ class_name || context_class.to_s.split('::').last
33
36
  else
34
- (context_class.is_a?(Class) ? context_class.name : context_class.to_s.camelize)
37
+ # For symbol parent, convert to string
38
+ context_class.to_s
35
39
  end
36
40
 
37
41
  # Store metadata for this indexing relationship
@@ -44,14 +48,38 @@ module Familia
44
48
  }
45
49
 
46
50
  # Generate finder methods on the context class
47
- if finder && context_class != :global
51
+ if finder && context_class.is_a?(Class)
48
52
  generate_context_finder_methods(context_class, field, index_name)
49
- elsif finder && context_class == :global
50
- generate_global_finder_methods(field, index_name)
51
53
  end
52
54
 
53
- # Generate instance methods for maintaining the index
54
- generate_indexing_instance_methods(context_class_name, field, index_name)
55
+ # Generate instance methods for relationship indexing
56
+ generate_relationship_index_methods(context_class_name, field, index_name)
57
+ end
58
+
59
+ # Define a class-level indexed lookup
60
+ #
61
+ # @param field [Symbol] The field to index on
62
+ # @param index_name [Symbol] Name of the index hash
63
+ # @param finder [Boolean] Whether to generate finder methods
64
+ #
65
+ # @example Class-level indexing (using class_ prefix convention)
66
+ # class_indexed_by :email, :email_lookup
67
+ # class_indexed_by :username, :username_lookup, finder: false
68
+ def class_indexed_by(field, index_name, finder: true)
69
+ # Store metadata for this indexing relationship
70
+ indexing_relationships << {
71
+ field: field,
72
+ context_class: self,
73
+ context_class_name: name,
74
+ index_name: index_name,
75
+ finder: finder
76
+ }
77
+
78
+ # Generate class-level finder methods if requested
79
+ generate_class_finder_methods(field, index_name) if finder
80
+
81
+ # Generate instance methods for class-level indexing
82
+ generate_direct_index_methods(field, index_name)
55
83
  end
56
84
 
57
85
  # Get all indexing relationships for this class
@@ -61,63 +89,49 @@ module Familia
61
89
 
62
90
  private
63
91
 
92
+ # Helper method to camelize a word without ActiveSupport dependency
93
+ def camelize_word(word)
94
+ word.to_s.split('_').map(&:capitalize).join
95
+ end
96
+
64
97
  # Generate finder methods on the context class (e.g., Customer.find_by_display_name)
65
98
  def generate_context_finder_methods(context_class, field, index_name)
66
99
  # Resolve context class if it's a symbol/string
67
- actual_context_class = context_class.is_a?(Class) ? context_class : Object.const_get(context_class.to_s.camelize)
100
+ actual_context_class = context_class.is_a?(Class) ? context_class : Object.const_get(camelize_word(context_class))
101
+
102
+ # Store reference to the indexed class for the finder methods
103
+ indexed_class = self
68
104
 
69
105
  # Generate finder method (e.g., Customer.find_by_display_name)
70
- actual_context_class.define_method("find_by_#{field}") do |field_value|
71
- index_key = "#{self.class.name.downcase}:#{identifier}:#{index_name}"
106
+ actual_context_class.define_singleton_method("find_by_#{field}") do |field_value|
107
+ index_key = "#{self.name.downcase}:#{index_name}"
72
108
  object_id = dbclient.hget(index_key, field_value.to_s)
73
109
 
74
110
  return nil unless object_id
75
111
 
76
- # Find the indexed class and instantiate the object
77
- indexed_class = nil
78
- self.class.const_get(:INDEXED_CLASSES, false)&.each do |klass|
79
- if klass.indexing_relationships.any? { |rel| rel[:index_name] == index_name }
80
- indexed_class = klass
81
- break
82
- end
83
- end
84
-
85
- indexed_class&.new(identifier: object_id)
112
+ indexed_class.new(object_id)
86
113
  end
87
114
 
88
115
  # Generate bulk finder method (e.g., Customer.find_all_by_display_name)
89
- actual_context_class.define_method("find_all_by_#{field}") do |field_values|
116
+ actual_context_class.define_singleton_method("find_all_by_#{field}") do |field_values|
90
117
  return [] if field_values.empty?
91
118
 
92
- index_key = "#{self.class.name.downcase}:#{identifier}:#{index_name}"
119
+ index_key = "#{self.name.downcase}:#{index_name}"
93
120
  object_ids = dbclient.hmget(index_key, *field_values.map(&:to_s))
94
121
 
95
122
  # Filter out nil values and instantiate objects
96
- found_objects = object_ids.compact.filter_map do |object_id|
97
- # Find the indexed class and instantiate the object
98
- indexed_class = nil
99
- self.class.const_get(:INDEXED_CLASSES, false)&.each do |klass|
100
- if klass.indexing_relationships.any? { |rel| rel[:index_name] == index_name }
101
- indexed_class = klass
102
- break
103
- end
104
- end
105
-
106
- indexed_class&.new(identifier: object_id)
107
- end
108
-
109
- found_objects
123
+ object_ids.compact.map { |object_id| indexed_class.new(object_id) }
110
124
  end
111
125
 
112
126
  # Generate method to get the index hash directly
113
- actual_context_class.define_method(index_name) do
114
- index_key = "#{self.class.name.downcase}:#{identifier}:#{index_name}"
115
- Familia::HashKey.new(nil, dbkey: index_key, logical_database: self.class.logical_database)
127
+ actual_context_class.define_singleton_method(index_name) do
128
+ index_key = "#{self.name.downcase}:#{index_name}"
129
+ Familia::HashKey.new(nil, dbkey: index_key, logical_database: logical_database)
116
130
  end
117
131
 
118
132
  # Generate method to rebuild the index
119
- actual_context_class.define_method("rebuild_#{index_name}") do
120
- index_key = "#{self.class.name.downcase}:#{identifier}:#{index_name}"
133
+ actual_context_class.define_singleton_method("rebuild_#{index_name}") do
134
+ index_key = "#{self.name.downcase}:#{index_name}"
121
135
 
122
136
  # Clear existing index
123
137
  dbclient.del(index_key)
@@ -128,38 +142,38 @@ module Familia
128
142
  end
129
143
  end
130
144
 
131
- # Generate global finder methods (when context is :global)
132
- def generate_global_finder_methods(field, index_name)
133
- # Generate global finder method (e.g., Domain.find_by_display_name_globally)
134
- define_method("find_by_#{field}_globally") do |field_value|
135
- index_key = "global:#{index_name}"
145
+ # Generate class-level finder methods
146
+ def generate_class_finder_methods(field, index_name)
147
+ # Generate class-level finder method (e.g., Domain.find_by_display_name)
148
+ define_singleton_method("find_by_#{field}") do |field_value|
149
+ index_key = "#{self.name.downcase}:#{index_name}"
136
150
  object_id = dbclient.hget(index_key, field_value.to_s)
137
151
 
138
152
  return nil unless object_id
139
153
 
140
- new(identifier: object_id)
154
+ new(object_id)
141
155
  end
142
156
 
143
- # Generate global bulk finder method
144
- define_method("find_all_by_#{field}_globally") do |field_values|
157
+ # Generate class-level bulk finder method
158
+ define_singleton_method("find_all_by_#{field}") do |field_values|
145
159
  return [] if field_values.empty?
146
160
 
147
- index_key = "global:#{index_name}"
161
+ index_key = "#{self.name.downcase}:#{index_name}"
148
162
  object_ids = dbclient.hmget(index_key, *field_values.map(&:to_s))
149
163
 
150
164
  # Filter out nil values and instantiate objects
151
- object_ids.compact.map { |object_id| new(identifier: object_id) }
165
+ object_ids.compact.map { |object_id| self.new(object_id) }
152
166
  end
153
167
 
154
- # Generate method to get the global index hash directly
155
- define_method("global_#{index_name}") do
156
- index_key = "global:#{index_name}"
168
+ # Generate method to get the class-level index hash directly
169
+ define_singleton_method("#{index_name}") do
170
+ index_key = "#{self.name.downcase}:#{index_name}"
157
171
  Familia::HashKey.new(nil, dbkey: index_key, logical_database: logical_database)
158
172
  end
159
173
 
160
- # Generate method to rebuild the global index
161
- define_method("rebuild_global_#{index_name}") do
162
- index_key = "global:#{index_name}"
174
+ # Generate method to rebuild the class-level index
175
+ define_singleton_method("rebuild_#{index_name}") do
176
+ index_key = "#{self.name.downcase}:#{index_name}"
163
177
 
164
178
  # Clear existing index
165
179
  dbclient.del(index_key)
@@ -170,74 +184,72 @@ module Familia
170
184
  end
171
185
  end
172
186
 
173
- # Generate instance methods for maintaining indexes
174
- def generate_indexing_instance_methods(context_class_name, field, index_name)
175
- # Method to add this object to a specific index
176
- # e.g., domain.add_to_customer_domain_index(customer)
177
- if context_class_name == 'global'
178
- # Global index methods
179
- define_method("add_to_global_#{index_name}") do
180
- index_key = "global:#{index_name}"
181
- field_value = send(field)
187
+ # Generate instance methods for class-level indexing (class_indexed_by)
188
+ def generate_direct_index_methods(field, index_name)
189
+ # Class-level index methods
190
+ define_method("add_to_class_#{index_name}") do
191
+ index_key = "#{self.class.name.downcase}:#{index_name}"
192
+ field_value = send(field)
182
193
 
183
- return unless field_value
194
+ return unless field_value
184
195
 
185
- dbclient.hset(index_key, field_value.to_s, identifier)
186
- end
196
+ dbclient.hset(index_key, field_value.to_s, identifier)
197
+ end
187
198
 
188
- define_method("remove_from_global_#{index_name}") do
189
- index_key = "global:#{index_name}"
190
- field_value = send(field)
199
+ define_method("remove_from_class_#{index_name}") do
200
+ index_key = "#{self.class.name.downcase}:#{index_name}"
201
+ field_value = send(field)
191
202
 
192
- return unless field_value
203
+ return unless field_value
193
204
 
194
- dbclient.hdel(index_key, field_value.to_s)
195
- end
205
+ dbclient.hdel(index_key, field_value.to_s)
206
+ end
196
207
 
197
- define_method("update_in_global_#{index_name}") do |old_field_value = nil|
198
- index_key = "global:#{index_name}"
199
- new_field_value = send(field)
208
+ define_method("update_in_class_#{index_name}") do |old_field_value = nil|
209
+ index_key = "#{self.class.name.downcase}:#{index_name}"
210
+ new_field_value = send(field)
200
211
 
201
- dbclient.multi do |tx|
202
- # Remove old value if provided
203
- tx.hdel(index_key, old_field_value.to_s) if old_field_value
212
+ dbclient.multi do |tx|
213
+ # Remove old value if provided
214
+ tx.hdel(index_key, old_field_value.to_s) if old_field_value
204
215
 
205
- # Add new value if present
206
- tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
207
- end
216
+ # Add new value if present
217
+ tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
208
218
  end
209
- else
210
- define_method("add_to_#{context_class_name.downcase}_#{index_name}") do |context_instance|
211
- index_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{index_name}"
212
- field_value = send(field)
219
+ end
220
+ end
213
221
 
214
- return unless field_value
222
+ # Generate instance methods for relationship indexing (indexed_by with parent:)
223
+ def generate_relationship_index_methods(context_class_name, field, index_name)
224
+ # All indexes are stored at class level - parent is only conceptual
225
+ define_method("add_to_#{context_class_name.downcase}_#{index_name}") do |context_instance = nil|
226
+ index_key = "#{self.class.name.downcase}:#{index_name}"
227
+ field_value = send(field)
215
228
 
216
- dbclient.hset(index_key, field_value.to_s, identifier)
217
- end
229
+ return unless field_value
218
230
 
219
- # Method to remove this object from a specific index
220
- define_method("remove_from_#{context_class_name.downcase}_#{index_name}") do |context_instance|
221
- index_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{index_name}"
222
- field_value = send(field)
231
+ dbclient.hset(index_key, field_value.to_s, identifier)
232
+ end
223
233
 
224
- return unless field_value
234
+ define_method("remove_from_#{context_class_name.downcase}_#{index_name}") do |context_instance = nil|
235
+ index_key = "#{self.class.name.downcase}:#{index_name}"
236
+ field_value = send(field)
225
237
 
226
- dbclient.hdel(index_key, field_value.to_s)
227
- end
238
+ return unless field_value
239
+
240
+ dbclient.hdel(index_key, field_value.to_s)
241
+ end
228
242
 
229
- # Method to update this object in a specific index (handles field value changes)
230
- define_method("update_in_#{context_class_name.downcase}_#{index_name}") do |context_instance, old_field_value = nil|
231
- index_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{index_name}"
232
- new_field_value = send(field)
243
+ define_method("update_in_#{context_class_name.downcase}_#{index_name}") do |context_instance = nil, old_field_value = nil|
244
+ index_key = "#{self.class.name.downcase}:#{index_name}"
245
+ new_field_value = send(field)
233
246
 
234
- dbclient.multi do |tx|
235
- # Remove old value if provided
236
- tx.hdel(index_key, old_field_value.to_s) if old_field_value
247
+ dbclient.multi do |tx|
248
+ # Remove old value if provided
249
+ tx.hdel(index_key, old_field_value.to_s) if old_field_value
237
250
 
238
- # Add new value if present
239
- tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
240
- end
251
+ # Add new value if present
252
+ tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
241
253
  end
242
254
  end
243
255
  end
@@ -245,49 +257,44 @@ module Familia
245
257
 
246
258
  # Instance methods for indexed objects
247
259
  module InstanceMethods
248
- # Update all indexes that this object participates in
260
+ # Update all indexes
249
261
  def update_all_indexes(old_values = {})
250
262
  return unless self.class.respond_to?(:indexing_relationships)
251
263
 
252
264
  self.class.indexing_relationships.each do |config|
253
265
  field = config[:field]
254
- context_class_name = config[:context_class_name]
255
266
  index_name = config[:index_name]
256
-
267
+ context_class = config[:context_class]
257
268
  old_field_value = old_values[field]
258
269
 
259
- if context_class_name == 'global'
260
- send("update_in_global_#{index_name}", old_field_value)
270
+ # Determine which update method to call
271
+ if context_class == self.class
272
+ # Class-level index (class_indexed_by)
273
+ send("update_in_class_#{index_name}", old_field_value)
261
274
  else
262
- # For non-global indexes, we'd need to know which context instances
263
- # this object should be indexed in. This is a simplified approach.
264
- # In practice, you'd need to track relationships or pass context.
275
+ # Relationship index (indexed_by with parent:)
276
+ context_class_name = config[:context_class_name].downcase
277
+ send("update_in_#{context_class_name}_#{index_name}", nil, old_field_value)
265
278
  end
266
279
  end
267
280
  end
268
281
 
269
- # Remove from all indexes (used during destroy)
282
+ # Remove from all indexes
270
283
  def remove_from_all_indexes
271
284
  return unless self.class.respond_to?(:indexing_relationships)
272
285
 
273
286
  self.class.indexing_relationships.each do |config|
274
- field = config[:field]
275
- context_class_name = config[:context_class_name]
276
287
  index_name = config[:index_name]
288
+ context_class = config[:context_class]
277
289
 
278
- if context_class_name == 'global'
279
- send("remove_from_global_#{index_name}")
290
+ # Determine which remove method to call
291
+ if context_class == self.class
292
+ # Class-level index (class_indexed_by)
293
+ send("remove_from_class_#{index_name}")
280
294
  else
281
- # For non-global indexes, we'd need to find all context instances
282
- # that have this object indexed. This is expensive but necessary for cleanup.
283
- pattern = "#{context_class_name.downcase}:*:#{index_name}"
284
- field_value = send(field)
285
-
286
- next unless field_value
287
-
288
- dbclient.scan_each(match: pattern) do |key|
289
- dbclient.hdel(key, field_value.to_s)
290
- end
295
+ # Relationship index (indexed_by with parent:)
296
+ context_class_name = config[:context_class_name].downcase
297
+ send("remove_from_#{context_class_name}_#{index_name}", nil)
291
298
  end
292
299
  end
293
300
  end
@@ -302,40 +309,23 @@ module Familia
302
309
 
303
310
  self.class.indexing_relationships.each do |config|
304
311
  field = config[:field]
305
- context_class_name = config[:context_class_name]
306
312
  index_name = config[:index_name]
313
+ context_class = config[:context_class]
307
314
  field_value = send(field)
308
315
 
309
316
  next unless field_value
310
317
 
311
- if context_class_name == 'global'
312
- index_key = "global:#{index_name}"
313
- if dbclient.hexists(index_key, field_value.to_s)
314
- memberships << {
315
- context_class: 'global',
316
- index_name: index_name,
317
- field: field,
318
- field_value: field_value,
319
- index_key: index_key
320
- }
321
- end
322
- else
323
- # Scan for all context instances that have this object indexed
324
- pattern = "#{context_class_name.downcase}:*:#{index_name}"
325
-
326
- dbclient.scan_each(match: pattern) do |key|
327
- if dbclient.hexists(key, field_value.to_s)
328
- context_id = key.split(':')[1]
329
- memberships << {
330
- context_class: context_class_name,
331
- context_id: context_id,
332
- index_name: index_name,
333
- field: field,
334
- field_value: field_value,
335
- index_key: key
336
- }
337
- end
338
- end
318
+ # All indexes are stored at class level
319
+ index_key = "#{self.class.name.downcase}:#{index_name}"
320
+ if dbclient.hexists(index_key, field_value.to_s)
321
+ memberships << {
322
+ context_class: context_class == self.class ? 'class' : config[:context_class_name].downcase,
323
+ index_name: index_name,
324
+ field: field,
325
+ field_value: field_value,
326
+ index_key: index_key,
327
+ type: context_class == self.class ? 'class_indexed_by' : 'indexed_by'
328
+ }
339
329
  end
340
330
  end
341
331
 
@@ -343,7 +333,7 @@ module Familia
343
333
  end
344
334
 
345
335
  # Check if this object is indexed in a specific context
346
- def indexed_in?(context_instance, index_name)
336
+ def indexed_in?(index_name)
347
337
  return false unless self.class.respond_to?(:indexing_relationships)
348
338
 
349
339
  config = self.class.indexing_relationships.find { |rel| rel[:index_name] == index_name }
@@ -353,17 +343,11 @@ module Familia
353
343
  field_value = send(field)
354
344
  return false unless field_value
355
345
 
356
- if config[:context_class_name] == 'global'
357
- index_key = "global:#{index_name}"
358
- else
359
- context_class_name = config[:context_class_name]
360
- index_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{index_name}"
361
- end
362
-
346
+ # For the cleaned-up API, all indexes are class-level
347
+ index_key = "#{self.class.name.downcase}:#{index_name}"
363
348
  dbclient.hexists(index_key, field_value.to_s)
364
349
  end
365
350
  end
366
-
367
351
  end
368
352
  end
369
353
  end
@@ -44,32 +44,15 @@ module Familia
44
44
  # Method to add this object to the owner's collection
45
45
  # e.g., domain.add_to_customer_domains(customer)
46
46
  define_method("add_to_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, score = nil|
47
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
48
-
49
- case type
50
- when :sorted_set
51
- score ||= calculate_membership_score(owner_class, collection_name)
52
- dbclient.zadd(collection_key, score, identifier)
53
- when :set
54
- dbclient.sadd(collection_key, identifier)
55
- when :list
56
- dbclient.lpush(collection_key, identifier)
57
- end
47
+ collection = owner_instance.send(collection_name)
48
+ collection.add(identifier, score: score)
58
49
  end
59
50
 
60
51
  # Method to remove this object from the owner's collection
61
52
  # e.g., domain.remove_from_customer_domains(customer)
62
53
  define_method("remove_from_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
63
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
64
-
65
- case type
66
- when :sorted_set
67
- dbclient.zrem(collection_key, identifier)
68
- when :set
69
- dbclient.srem(collection_key, identifier)
70
- when :list
71
- dbclient.lrem(collection_key, 0, identifier)
72
- end
54
+ collection = owner_instance.send(collection_name)
55
+ collection.remove(identifier)
73
56
  end
74
57
 
75
58
  # Method to check if this object is in the owner's collection
@@ -77,6 +60,9 @@ module Familia
77
60
  define_method("in_#{owner_class_name_lower}_#{collection_name}?") do |owner_instance|
78
61
  collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
79
62
 
63
+ # TODO: We should be able to reduce this to a single method call on the DataType class
64
+ # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
65
+ # and List classes have a `remove` method that implements the correct behaviour).
80
66
  case type
81
67
  when :sorted_set
82
68
  !dbclient.zscore(collection_key, identifier).nil?
@@ -123,6 +109,9 @@ module Familia
123
109
  define_method("add_to_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, score = nil|
124
110
  collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
125
111
 
112
+ # TODO: We should be able to reduce this to a single method call on the DataType class
113
+ # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
114
+ # and List classes have a `remove` method that implements the correct behaviour).
126
115
  case type
127
116
  when :sorted_set
128
117
  # Find the owner class from the stored config
@@ -144,6 +133,9 @@ module Familia
144
133
  define_method("remove_from_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
145
134
  collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
146
135
 
136
+ # TODO: We should be able to reduce this to a single method call on the DataType class
137
+ # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
138
+ # and List classes have a `remove` method that implements the correct behaviour).
147
139
  case type
148
140
  when :sorted_set
149
141
  dbclient.zrem(collection_key, identifier)
@@ -159,6 +151,9 @@ module Familia
159
151
  define_method("in_#{owner_class_name_lower}_#{collection_name}?") do |owner_instance|
160
152
  collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
161
153
 
154
+ # TODO: We should be able to reduce this to a single method call on the DataType class
155
+ # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
156
+ # and List classes have a `remove` method that implements the correct behaviour).
162
157
  case type
163
158
  when :sorted_set
164
159
  dbclient.zscore(collection_key, identifier) != nil
@@ -496,7 +491,6 @@ module Familia
496
491
  true
497
492
  end
498
493
  end
499
-
500
494
  end
501
495
  end
502
496
  end
@@ -303,17 +303,15 @@ module Familia
303
303
  permission_bits = decoded[:permissions]
304
304
 
305
305
  # Check if this member has the required permission bits
306
- if (permission_bits & required_bits) == required_bits
307
- valid_members << [score, member]
308
- end
306
+ valid_members << [score, member] if (permission_bits & required_bits) == required_bits
309
307
  end
310
308
 
311
309
  # Recreate filtered collection if we have valid members
312
- if valid_members.any?
313
- dbclient.zadd(filtered_key, valid_members)
314
- dbclient.expire(filtered_key, 300) # Temporary key cleanup
315
- filtered_keys << filtered_key
316
- end
310
+ next unless valid_members.any?
311
+
312
+ dbclient.zadd(filtered_key, valid_members)
313
+ dbclient.expire(filtered_key, 300) # Temporary key cleanup
314
+ filtered_keys << filtered_key
317
315
  end
318
316
 
319
317
  filtered_keys
@@ -341,9 +339,7 @@ module Familia
341
339
  permission_bits = decoded[:permissions]
342
340
 
343
341
  # Check if this member has the required permission bits
344
- if (permission_bits & required_bits) == required_bits
345
- valid_members << [score, member]
346
- end
342
+ valid_members << [score, member] if (permission_bits & required_bits) == required_bits
347
343
  end
348
344
 
349
345
  # Create filtered collection
@@ -613,7 +609,6 @@ module Familia
613
609
  collection_info
614
610
  end
615
611
  end
616
-
617
612
  end
618
613
  end
619
614
  end
@@ -40,7 +40,7 @@ module Familia
40
40
  configure: 0b00010000, # 16 - Change settings
41
41
  delete: 0b00100000, # 32 - Remove items
42
42
  transfer: 0b01000000, # 64 - Change ownership
43
- admin: 0b10000000 # 128 - Full control
43
+ admin: 0b10000000, # 128 - Full control
44
44
  }.freeze
45
45
 
46
46
  # Predefined permission combinations
@@ -60,7 +60,6 @@ module Familia
60
60
  owner: 0b11111111 # All permissions
61
61
  }.freeze
62
62
 
63
-
64
63
  class << self
65
64
  # Get permission bit flag value for a permission symbol
66
65
  #
@@ -93,7 +92,6 @@ module Familia
93
92
  }
94
93
  end
95
94
 
96
-
97
95
  # Encode a timestamp and permissions into a Redis score
98
96
  #
99
97
  # @param timestamp [Time, Integer] The timestamp to encode