familia 2.0.0.pre5 → 2.0.0.pre7
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 +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,370 @@
|
|
1
|
+
# lib/familia/features/relationships/indexing.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
# Indexing module for indexed_by relationships using Redis hashes
|
7
|
+
# Provides O(1) lookups for finding objects by field values
|
8
|
+
module Indexing
|
9
|
+
# Class-level indexing configurations
|
10
|
+
def self.included(base)
|
11
|
+
base.extend ClassMethods
|
12
|
+
base.include InstanceMethods
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Define an indexed_by relationship for fast lookups
|
18
|
+
#
|
19
|
+
# @param field [Symbol] The field to index on
|
20
|
+
# @param context [Class, Symbol] The context class that owns the index
|
21
|
+
# @param index_name [Symbol] Name of the index hash
|
22
|
+
# @param finder [Boolean] Whether to generate finder methods
|
23
|
+
#
|
24
|
+
# @example Basic indexing
|
25
|
+
# indexed_by :display_name, context: Customer, index_name: :domain_index
|
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'
|
33
|
+
else
|
34
|
+
(context_class.is_a?(Class) ? context_class.name : context_class.to_s.camelize)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Store metadata for this indexing relationship
|
38
|
+
indexing_relationships << {
|
39
|
+
field: field,
|
40
|
+
context_class: context_class,
|
41
|
+
context_class_name: context_class_name,
|
42
|
+
index_name: index_name,
|
43
|
+
finder: finder
|
44
|
+
}
|
45
|
+
|
46
|
+
# Generate finder methods on the context class
|
47
|
+
if finder && context_class != :global
|
48
|
+
generate_context_finder_methods(context_class, field, index_name)
|
49
|
+
elsif finder && context_class == :global
|
50
|
+
generate_global_finder_methods(field, index_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Generate instance methods for maintaining the index
|
54
|
+
generate_indexing_instance_methods(context_class_name, field, index_name)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get all indexing relationships for this class
|
58
|
+
def indexing_relationships
|
59
|
+
@indexing_relationships ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Generate finder methods on the context class (e.g., Customer.find_by_display_name)
|
65
|
+
def generate_context_finder_methods(context_class, field, index_name)
|
66
|
+
# 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)
|
68
|
+
|
69
|
+
# 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}"
|
72
|
+
object_id = dbclient.hget(index_key, field_value.to_s)
|
73
|
+
|
74
|
+
return nil unless object_id
|
75
|
+
|
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)
|
86
|
+
end
|
87
|
+
|
88
|
+
# 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|
|
90
|
+
return [] if field_values.empty?
|
91
|
+
|
92
|
+
index_key = "#{self.class.name.downcase}:#{identifier}:#{index_name}"
|
93
|
+
object_ids = dbclient.hmget(index_key, *field_values.map(&:to_s))
|
94
|
+
|
95
|
+
# 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
|
110
|
+
end
|
111
|
+
|
112
|
+
# 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)
|
116
|
+
end
|
117
|
+
|
118
|
+
# 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}"
|
121
|
+
|
122
|
+
# Clear existing index
|
123
|
+
dbclient.del(index_key)
|
124
|
+
|
125
|
+
# This is a simplified version - in practice, you'd need to iterate
|
126
|
+
# through all objects that should be in this index
|
127
|
+
# Implementation would depend on how you track which objects belong to this context
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
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}"
|
136
|
+
object_id = dbclient.hget(index_key, field_value.to_s)
|
137
|
+
|
138
|
+
return nil unless object_id
|
139
|
+
|
140
|
+
new(identifier: object_id)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Generate global bulk finder method
|
144
|
+
define_method("find_all_by_#{field}_globally") do |field_values|
|
145
|
+
return [] if field_values.empty?
|
146
|
+
|
147
|
+
index_key = "global:#{index_name}"
|
148
|
+
object_ids = dbclient.hmget(index_key, *field_values.map(&:to_s))
|
149
|
+
|
150
|
+
# Filter out nil values and instantiate objects
|
151
|
+
object_ids.compact.map { |object_id| new(identifier: object_id) }
|
152
|
+
end
|
153
|
+
|
154
|
+
# Generate method to get the global index hash directly
|
155
|
+
define_method("global_#{index_name}") do
|
156
|
+
index_key = "global:#{index_name}"
|
157
|
+
Familia::HashKey.new(nil, dbkey: index_key, logical_database: logical_database)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Generate method to rebuild the global index
|
161
|
+
define_method("rebuild_global_#{index_name}") do
|
162
|
+
index_key = "global:#{index_name}"
|
163
|
+
|
164
|
+
# Clear existing index
|
165
|
+
dbclient.del(index_key)
|
166
|
+
|
167
|
+
# Rebuild from all existing objects
|
168
|
+
# This would need to scan through all objects of this class
|
169
|
+
# Implementation depends on how objects are stored/tracked
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
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)
|
182
|
+
|
183
|
+
return unless field_value
|
184
|
+
|
185
|
+
dbclient.hset(index_key, field_value.to_s, identifier)
|
186
|
+
end
|
187
|
+
|
188
|
+
define_method("remove_from_global_#{index_name}") do
|
189
|
+
index_key = "global:#{index_name}"
|
190
|
+
field_value = send(field)
|
191
|
+
|
192
|
+
return unless field_value
|
193
|
+
|
194
|
+
dbclient.hdel(index_key, field_value.to_s)
|
195
|
+
end
|
196
|
+
|
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)
|
200
|
+
|
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
|
204
|
+
|
205
|
+
# Add new value if present
|
206
|
+
tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
|
207
|
+
end
|
208
|
+
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)
|
213
|
+
|
214
|
+
return unless field_value
|
215
|
+
|
216
|
+
dbclient.hset(index_key, field_value.to_s, identifier)
|
217
|
+
end
|
218
|
+
|
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)
|
223
|
+
|
224
|
+
return unless field_value
|
225
|
+
|
226
|
+
dbclient.hdel(index_key, field_value.to_s)
|
227
|
+
end
|
228
|
+
|
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)
|
233
|
+
|
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
|
237
|
+
|
238
|
+
# Add new value if present
|
239
|
+
tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Instance methods for indexed objects
|
247
|
+
module InstanceMethods
|
248
|
+
# Update all indexes that this object participates in
|
249
|
+
def update_all_indexes(old_values = {})
|
250
|
+
return unless self.class.respond_to?(:indexing_relationships)
|
251
|
+
|
252
|
+
self.class.indexing_relationships.each do |config|
|
253
|
+
field = config[:field]
|
254
|
+
context_class_name = config[:context_class_name]
|
255
|
+
index_name = config[:index_name]
|
256
|
+
|
257
|
+
old_field_value = old_values[field]
|
258
|
+
|
259
|
+
if context_class_name == 'global'
|
260
|
+
send("update_in_global_#{index_name}", old_field_value)
|
261
|
+
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.
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Remove from all indexes (used during destroy)
|
270
|
+
def remove_from_all_indexes
|
271
|
+
return unless self.class.respond_to?(:indexing_relationships)
|
272
|
+
|
273
|
+
self.class.indexing_relationships.each do |config|
|
274
|
+
field = config[:field]
|
275
|
+
context_class_name = config[:context_class_name]
|
276
|
+
index_name = config[:index_name]
|
277
|
+
|
278
|
+
if context_class_name == 'global'
|
279
|
+
send("remove_from_global_#{index_name}")
|
280
|
+
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
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Get all indexes this object appears in
|
296
|
+
#
|
297
|
+
# @return [Array<Hash>] Array of index information
|
298
|
+
def indexing_memberships
|
299
|
+
return [] unless self.class.respond_to?(:indexing_relationships)
|
300
|
+
|
301
|
+
memberships = []
|
302
|
+
|
303
|
+
self.class.indexing_relationships.each do |config|
|
304
|
+
field = config[:field]
|
305
|
+
context_class_name = config[:context_class_name]
|
306
|
+
index_name = config[:index_name]
|
307
|
+
field_value = send(field)
|
308
|
+
|
309
|
+
next unless field_value
|
310
|
+
|
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
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
memberships
|
343
|
+
end
|
344
|
+
|
345
|
+
# Check if this object is indexed in a specific context
|
346
|
+
def indexed_in?(context_instance, index_name)
|
347
|
+
return false unless self.class.respond_to?(:indexing_relationships)
|
348
|
+
|
349
|
+
config = self.class.indexing_relationships.find { |rel| rel[:index_name] == index_name }
|
350
|
+
return false unless config
|
351
|
+
|
352
|
+
field = config[:field]
|
353
|
+
field_value = send(field)
|
354
|
+
return false unless field_value
|
355
|
+
|
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
|
+
|
363
|
+
dbclient.hexists(index_key, field_value.to_s)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|