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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +13 -0
- data/.github/workflows/docs.yml +1 -1
- data/.gitignore +9 -9
- data/.rubocop.yml +19 -0
- data/.yardopts +22 -1
- data/CHANGELOG.md +184 -0
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +97 -2
- data/changelog.d/README.md +66 -0
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +29 -0
- data/docs/archive/.gitignore +2 -0
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
- data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
- data/docs/archive/FAMILIA_UPDATE.md +226 -0
- data/docs/archive/README.md +67 -0
- data/docs/guides/.gitignore +2 -0
- data/docs/{wiki → guides}/Feature-System-Guide.md +0 -15
- data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
- data/docs/guides/relationships-methods.md +266 -0
- data/examples/relationships_basic.rb +90 -157
- data/familia.gemspec +4 -4
- data/lib/familia/connection.rb +4 -21
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
- data/lib/familia/features/external_identifiers.rb +111 -0
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
- data/lib/familia/features/object_identifiers.rb +194 -0
- data/lib/familia/features/relationships/cascading.rb +0 -1
- data/lib/familia/features/relationships/indexing.rb +160 -176
- data/lib/familia/features/relationships/membership.rb +16 -22
- data/lib/familia/features/relationships/querying.rb +7 -12
- data/lib/familia/features/relationships/score_encoding.rb +1 -3
- data/lib/familia/features/relationships/tracking.rb +61 -22
- data/lib/familia/features/relationships.rb +15 -8
- data/lib/familia/features/transient_fields.rb +8 -10
- data/lib/familia/features.rb +16 -13
- data/lib/familia/horreum/core/serialization.rb +2 -5
- data/lib/familia/horreum/subclass/definition.rb +36 -0
- data/lib/familia/horreum.rb +15 -24
- data/lib/familia/version.rb +1 -3
- data/setup.cfg +12 -0
- data/try/core/errors_try.rb +1 -1
- data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
- data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
- data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
- data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
- data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
- data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
- data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
- data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
- data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
- data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +339 -0
- data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
- data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
- data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
- data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
- data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
- data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +7 -6
- data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
- data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
- data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
- data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
- metadata +80 -60
- /data/docs/{wiki → guides}/API-Reference.md +0 -0
- /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
- /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
- /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
- /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
- /data/docs/{wiki → guides}/Home.md +0 -0
- /data/docs/{wiki → guides}/Implementation-Guide.md +0 -0
- /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Security-Model.md +0 -0
- /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
- /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,
|
25
|
+
# indexed_by :display_name, parent: Customer, index_name: :domain_index
|
26
26
|
#
|
27
|
-
# @example
|
28
|
-
# indexed_by :
|
29
|
-
def indexed_by(field, index_name,
|
30
|
-
context_class =
|
31
|
-
context_class_name = if context_class
|
32
|
-
|
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
|
-
|
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
|
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
|
54
|
-
|
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
|
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.
|
71
|
-
index_key = "#{self.
|
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
|
-
|
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.
|
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.
|
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
|
-
|
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.
|
114
|
-
index_key = "#{self.
|
115
|
-
Familia::HashKey.new(nil, dbkey: index_key, 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.
|
120
|
-
index_key = "#{self.
|
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
|
132
|
-
def
|
133
|
-
# Generate
|
134
|
-
|
135
|
-
index_key = "
|
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(
|
154
|
+
new(object_id)
|
141
155
|
end
|
142
156
|
|
143
|
-
# Generate
|
144
|
-
|
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 = "
|
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(
|
165
|
+
object_ids.compact.map { |object_id| self.new(object_id) }
|
152
166
|
end
|
153
167
|
|
154
|
-
# Generate method to get the
|
155
|
-
|
156
|
-
index_key = "
|
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
|
161
|
-
|
162
|
-
index_key = "
|
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
|
174
|
-
def
|
175
|
-
#
|
176
|
-
#
|
177
|
-
|
178
|
-
|
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
|
-
|
194
|
+
return unless field_value
|
184
195
|
|
185
|
-
|
186
|
-
|
196
|
+
dbclient.hset(index_key, field_value.to_s, identifier)
|
197
|
+
end
|
187
198
|
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
203
|
+
return unless field_value
|
193
204
|
|
194
|
-
|
195
|
-
|
205
|
+
dbclient.hdel(index_key, field_value.to_s)
|
206
|
+
end
|
196
207
|
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
206
|
-
|
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
|
-
|
210
|
-
|
211
|
-
index_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{index_name}"
|
212
|
-
field_value = send(field)
|
219
|
+
end
|
220
|
+
end
|
213
221
|
|
214
|
-
|
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
|
-
|
217
|
-
end
|
229
|
+
return unless field_value
|
218
230
|
|
219
|
-
|
220
|
-
|
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
|
-
|
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
|
-
|
227
|
-
|
238
|
+
return unless field_value
|
239
|
+
|
240
|
+
dbclient.hdel(index_key, field_value.to_s)
|
241
|
+
end
|
228
242
|
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
239
|
-
|
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
|
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
|
-
|
260
|
-
|
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
|
-
#
|
263
|
-
|
264
|
-
#
|
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
|
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
|
-
|
279
|
-
|
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
|
-
#
|
282
|
-
|
283
|
-
|
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
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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?(
|
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
|
-
|
357
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
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
|