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,379 @@
|
|
1
|
+
# lib/familia/features/relationships/tracking.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
# Tracking module for tracked_in relationships using Redis sorted sets
|
7
|
+
# Provides multi-presence support where objects can exist in multiple collections
|
8
|
+
module Tracking
|
9
|
+
# Class-level tracking configurations
|
10
|
+
def self.included(base)
|
11
|
+
base.extend ClassMethods
|
12
|
+
base.include InstanceMethods
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Simple singularize method (basic implementation)
|
18
|
+
def singularize_word(word)
|
19
|
+
word = word.to_s
|
20
|
+
# Basic English pluralization rules (simplified)
|
21
|
+
if word.end_with?('ies')
|
22
|
+
"#{word[0..-4]}y"
|
23
|
+
elsif word.end_with?('es') && word.length > 3
|
24
|
+
word[0..-3]
|
25
|
+
elsif word.end_with?('s') && word.length > 1
|
26
|
+
word[0..-2]
|
27
|
+
else
|
28
|
+
word
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Simple camelize method (basic implementation)
|
33
|
+
def camelize_word(word)
|
34
|
+
word.to_s.split('_').map(&:capitalize).join
|
35
|
+
end
|
36
|
+
|
37
|
+
# Define a tracked_in relationship
|
38
|
+
#
|
39
|
+
# @param context_class [Class, Symbol] The class that owns the collection
|
40
|
+
# @param collection_name [Symbol] Name of the collection
|
41
|
+
# @param score [Symbol, Proc, nil] How to calculate the score
|
42
|
+
# @param on_destroy [Symbol] What to do when object is destroyed (:remove, :ignore)
|
43
|
+
#
|
44
|
+
# @example Basic tracking
|
45
|
+
# tracked_in Customer, :domains, score: :created_at
|
46
|
+
#
|
47
|
+
# @example Multi-presence tracking
|
48
|
+
# tracked_in Customer, :domains, score: -> { permission_encode(created_at, permission_level) }
|
49
|
+
# tracked_in Team, :domains, score: :added_at
|
50
|
+
# tracked_in Organization, :all_domains, score: :created_at
|
51
|
+
def tracked_in(context_class, collection_name, score: nil, on_destroy: :remove)
|
52
|
+
# Handle special :global context
|
53
|
+
if context_class == :global
|
54
|
+
context_class_name = 'Global'
|
55
|
+
elsif context_class.is_a?(Class)
|
56
|
+
class_name = context_class.name
|
57
|
+
context_class_name = if class_name.include?('::')
|
58
|
+
# Extract the last part after the last ::
|
59
|
+
class_name.split('::').last
|
60
|
+
else
|
61
|
+
class_name
|
62
|
+
end
|
63
|
+
# Extract just the class name, handling anonymous classes
|
64
|
+
else
|
65
|
+
context_class_name = camelize_word(context_class)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Store metadata for this tracking relationship
|
69
|
+
tracking_relationships << {
|
70
|
+
context_class: context_class,
|
71
|
+
context_class_name: context_class_name,
|
72
|
+
collection_name: collection_name,
|
73
|
+
score: score,
|
74
|
+
on_destroy: on_destroy
|
75
|
+
}
|
76
|
+
|
77
|
+
# Generate class methods on the context class (skip for global)
|
78
|
+
if context_class == :global
|
79
|
+
generate_global_class_methods(self, collection_name)
|
80
|
+
else
|
81
|
+
generate_context_class_methods(context_class, collection_name)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Generate instance methods on this class
|
85
|
+
generate_tracking_instance_methods(context_class_name, collection_name, score)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get all tracking relationships for this class
|
89
|
+
def tracking_relationships
|
90
|
+
@tracking_relationships ||= []
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Generate global collection methods (e.g., Domain.global_all_domains)
|
96
|
+
def generate_global_class_methods(target_class, collection_name)
|
97
|
+
# Generate global collection getter method
|
98
|
+
target_class.define_singleton_method("global_#{collection_name}") do
|
99
|
+
collection_key = "global:#{collection_name}"
|
100
|
+
Familia::SortedSet.new(nil, dbkey: collection_key, logical_database: logical_database)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Generate global add method (e.g., Domain.add_to_global_all_domains)
|
104
|
+
target_class.define_singleton_method("add_to_#{collection_name}") do |item, score = nil|
|
105
|
+
collection = send("global_#{collection_name}")
|
106
|
+
|
107
|
+
# Calculate score if not provided
|
108
|
+
score ||= if item.respond_to?(:calculate_tracking_score)
|
109
|
+
item.calculate_tracking_score(:global, collection_name)
|
110
|
+
else
|
111
|
+
item.current_score
|
112
|
+
end
|
113
|
+
|
114
|
+
# Ensure score is never nil
|
115
|
+
score = item.current_score if score.nil?
|
116
|
+
|
117
|
+
collection.add(score, item.identifier)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Generate global remove method
|
121
|
+
target_class.define_singleton_method("remove_from_#{collection_name}") do |item|
|
122
|
+
collection = send("global_#{collection_name}")
|
123
|
+
collection.delete(item.identifier)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Generate methods on the context class (e.g., Customer.domains)
|
128
|
+
def generate_context_class_methods(context_class, collection_name)
|
129
|
+
# Resolve context class if it's a symbol/string
|
130
|
+
actual_context_class = context_class.is_a?(Class) ? context_class : Object.const_get(camelize_word(context_class))
|
131
|
+
|
132
|
+
# Generate collection getter method
|
133
|
+
actual_context_class.define_method(collection_name) do
|
134
|
+
collection_key = "#{self.class.name.downcase}:#{identifier}:#{collection_name}"
|
135
|
+
Familia::SortedSet.new(nil, dbkey: collection_key, logical_database: self.class.logical_database)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Generate add method (e.g., Customer#add_domain)
|
139
|
+
actual_context_class.define_method("add_#{singularize_word(collection_name)}") do |item, score = nil|
|
140
|
+
collection = send(collection_name)
|
141
|
+
|
142
|
+
# Calculate score if not provided
|
143
|
+
score ||= if item.respond_to?(:calculate_tracking_score)
|
144
|
+
item.calculate_tracking_score(self.class, collection_name)
|
145
|
+
else
|
146
|
+
item.current_score
|
147
|
+
end
|
148
|
+
|
149
|
+
# Ensure score is never nil
|
150
|
+
score = item.current_score if score.nil?
|
151
|
+
|
152
|
+
collection.add(score, item.identifier)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Generate remove method (e.g., Customer#remove_domain)
|
156
|
+
actual_context_class.define_method("remove_#{singularize_word(collection_name)}") do |item|
|
157
|
+
collection = send(collection_name)
|
158
|
+
collection.delete(item.identifier)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Generate bulk add method (e.g., Customer#add_domains)
|
162
|
+
actual_context_class.define_method("add_#{collection_name}") do |items|
|
163
|
+
return if items.empty?
|
164
|
+
|
165
|
+
collection = send(collection_name)
|
166
|
+
|
167
|
+
# Prepare batch data
|
168
|
+
batch_data = items.map do |item|
|
169
|
+
score = if item.respond_to?(:calculate_tracking_score)
|
170
|
+
item.calculate_tracking_score(self.class, collection_name)
|
171
|
+
else
|
172
|
+
item.current_score
|
173
|
+
end
|
174
|
+
# Ensure score is never nil
|
175
|
+
score = item.current_score if score.nil?
|
176
|
+
{ member: item.identifier, score: score }
|
177
|
+
end
|
178
|
+
|
179
|
+
# Use batch operation from RedisOperations
|
180
|
+
collection.dbclient.pipelined do |pipeline|
|
181
|
+
batch_data.each do |data|
|
182
|
+
pipeline.zadd(collection.rediskey, data[:score], data[:member])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Generate query methods with score filtering
|
188
|
+
actual_context_class.define_method("#{collection_name}_with_permission") do |min_permission = :read|
|
189
|
+
collection = send(collection_name)
|
190
|
+
permission_score = ScoreEncoding.permission_encode(0, min_permission)
|
191
|
+
|
192
|
+
collection.zrangebyscore(permission_score, '+inf', with_scores: true)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Generate instance methods on the tracked class
|
197
|
+
def generate_tracking_instance_methods(context_class_name, collection_name, _score_calculator)
|
198
|
+
# Method to check if this object is in a specific collection
|
199
|
+
# e.g., domain.in_customer_domains?(customer)
|
200
|
+
define_method("in_#{context_class_name.downcase}_#{collection_name}?") do |context_instance|
|
201
|
+
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
202
|
+
dbclient.zscore(collection_key, identifier) != nil
|
203
|
+
end
|
204
|
+
|
205
|
+
# Method to add this object to a specific collection
|
206
|
+
# e.g., domain.add_to_customer_domains(customer, score)
|
207
|
+
define_method("add_to_#{context_class_name.downcase}_#{collection_name}") do |context_instance, score = nil|
|
208
|
+
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
209
|
+
|
210
|
+
score ||= calculate_tracking_score(context_class_name, collection_name)
|
211
|
+
|
212
|
+
# Ensure score is never nil
|
213
|
+
score = current_score if score.nil?
|
214
|
+
|
215
|
+
dbclient.zadd(collection_key, score, identifier)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Method to remove this object from a specific collection
|
219
|
+
# e.g., domain.remove_from_customer_domains(customer)
|
220
|
+
define_method("remove_from_#{context_class_name.downcase}_#{collection_name}") do |context_instance|
|
221
|
+
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
222
|
+
dbclient.zrem(collection_key, identifier)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Method to get score in a specific collection
|
226
|
+
# e.g., domain.score_in_customer_domains(customer)
|
227
|
+
define_method("score_in_#{context_class_name.downcase}_#{collection_name}") do |context_instance|
|
228
|
+
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
229
|
+
dbclient.zscore(collection_key, identifier)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Method to update score in a specific collection
|
233
|
+
# e.g., domain.update_score_in_customer_domains(customer, new_score)
|
234
|
+
define_method("update_score_in_#{context_class_name.downcase}_#{collection_name}") do |context_instance, new_score|
|
235
|
+
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
236
|
+
dbclient.zadd(collection_key, new_score, identifier, xx: true) # Only update existing
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Instance methods for tracked objects
|
242
|
+
module InstanceMethods
|
243
|
+
# Calculate the appropriate score for a tracking relationship
|
244
|
+
#
|
245
|
+
# @param context_class [Class] The context class (e.g., Customer)
|
246
|
+
# @param collection_name [Symbol] The collection name (e.g., :domains)
|
247
|
+
# @return [Float] Calculated score
|
248
|
+
def calculate_tracking_score(context_class, collection_name)
|
249
|
+
# Find the tracking configuration
|
250
|
+
tracking_config = self.class.tracking_relationships.find do |config|
|
251
|
+
config[:context_class] == context_class && config[:collection_name] == collection_name
|
252
|
+
end
|
253
|
+
|
254
|
+
return current_score unless tracking_config
|
255
|
+
|
256
|
+
score_calculator = tracking_config[:score]
|
257
|
+
|
258
|
+
case score_calculator
|
259
|
+
when Symbol
|
260
|
+
# Field name or method name
|
261
|
+
if respond_to?(score_calculator)
|
262
|
+
value = send(score_calculator)
|
263
|
+
if value.respond_to?(:to_f)
|
264
|
+
value.to_f
|
265
|
+
elsif value.respond_to?(:to_i)
|
266
|
+
encode_score(value, 0)
|
267
|
+
else
|
268
|
+
current_score
|
269
|
+
end
|
270
|
+
else
|
271
|
+
current_score
|
272
|
+
end
|
273
|
+
when Proc
|
274
|
+
# Execute proc in context of this instance
|
275
|
+
result = instance_exec(&score_calculator)
|
276
|
+
# Ensure we get a numeric result
|
277
|
+
if result.nil?
|
278
|
+
current_score
|
279
|
+
elsif result.respond_to?(:to_f)
|
280
|
+
result.to_f
|
281
|
+
else
|
282
|
+
current_score
|
283
|
+
end
|
284
|
+
when Numeric
|
285
|
+
score_calculator.to_f
|
286
|
+
else
|
287
|
+
current_score
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Update presence in all tracked collections atomically
|
292
|
+
def update_all_tracking_collections
|
293
|
+
return unless self.class.respond_to?(:tracking_relationships)
|
294
|
+
|
295
|
+
[]
|
296
|
+
|
297
|
+
self.class.tracking_relationships.each do |config|
|
298
|
+
config[:context_class_name]
|
299
|
+
config[:collection_name]
|
300
|
+
|
301
|
+
# This is a simplified version - in practice, you'd need to know
|
302
|
+
# which specific instances this object should be tracked in
|
303
|
+
# For now, we'll skip the automatic update and rely on explicit calls
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Remove from all tracking collections (used during destroy)
|
308
|
+
def remove_from_all_tracking_collections
|
309
|
+
return unless self.class.respond_to?(:tracking_relationships)
|
310
|
+
|
311
|
+
# Get all possible collection keys this object might be in
|
312
|
+
# This is expensive but necessary for cleanup
|
313
|
+
redis_conn = redis
|
314
|
+
pattern = '*:*:*' # This could be optimized with better key patterns
|
315
|
+
|
316
|
+
cursor = 0
|
317
|
+
matching_keys = []
|
318
|
+
|
319
|
+
loop do
|
320
|
+
cursor, keys = redis_conn.scan(cursor, match: pattern, count: 1000)
|
321
|
+
matching_keys.concat(keys)
|
322
|
+
break if cursor.zero?
|
323
|
+
end
|
324
|
+
|
325
|
+
# Filter keys that might contain this object and remove it
|
326
|
+
redis_conn.pipelined do |pipeline|
|
327
|
+
matching_keys.each do |key|
|
328
|
+
# Check if this key matches any of our tracking relationships
|
329
|
+
self.class.tracking_relationships.each do |config|
|
330
|
+
context_class_name = config[:context_class_name].downcase
|
331
|
+
collection_name = config[:collection_name]
|
332
|
+
|
333
|
+
if key.include?(context_class_name) && key.include?(collection_name.to_s)
|
334
|
+
pipeline.zrem(key, identifier)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Get all collections this object appears in
|
342
|
+
#
|
343
|
+
# @return [Array<Hash>] Array of collection information
|
344
|
+
def tracking_collections_membership
|
345
|
+
return [] unless self.class.respond_to?(:tracking_relationships)
|
346
|
+
|
347
|
+
memberships = []
|
348
|
+
|
349
|
+
self.class.tracking_relationships.each do |config|
|
350
|
+
context_class_name = config[:context_class_name]
|
351
|
+
collection_name = config[:collection_name]
|
352
|
+
|
353
|
+
# Find all instances of context_class where this object appears
|
354
|
+
# This is simplified - in practice you'd need a more efficient approach
|
355
|
+
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
356
|
+
|
357
|
+
dbclient.scan_each(match: pattern) do |key|
|
358
|
+
score = dbclient.zscore(key, identifier)
|
359
|
+
if score
|
360
|
+
context_id = key.split(':')[1]
|
361
|
+
memberships << {
|
362
|
+
context_class: context_class_name,
|
363
|
+
context_id: context_id,
|
364
|
+
collection_name: collection_name,
|
365
|
+
score: score,
|
366
|
+
decoded_score: decode_score(score)
|
367
|
+
}
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
memberships
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|