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,503 @@
|
|
1
|
+
# lib/familia/features/relationships/membership.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
# Membership module for member_of relationships
|
7
|
+
# Provides collision-free method naming by including collection names
|
8
|
+
module Membership
|
9
|
+
# Class-level membership configurations
|
10
|
+
def self.included(base)
|
11
|
+
base.extend ClassMethods
|
12
|
+
base.include InstanceMethods
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Define a member_of relationship
|
18
|
+
#
|
19
|
+
# @param owner_class [Class] The class that owns the collection
|
20
|
+
# @param collection_name [Symbol] Name of the collection on the owner
|
21
|
+
# @param score [Symbol, Proc, nil] How to calculate the score for sorted sets
|
22
|
+
# @param type [Symbol] Type of Redis collection (:sorted_set, :set, :list)
|
23
|
+
#
|
24
|
+
# @example Basic membership
|
25
|
+
# member_of Customer, :domains
|
26
|
+
#
|
27
|
+
# @example Membership with scoring
|
28
|
+
# member_of Team, :projects, score: -> { permission_encode(Time.now, permission_level) }
|
29
|
+
def member_of(owner_class, collection_name, score: nil, type: :sorted_set)
|
30
|
+
owner_class_name = owner_class.is_a?(Class) ? owner_class.name : owner_class.to_s.camelize
|
31
|
+
|
32
|
+
# Store metadata for this membership relationship
|
33
|
+
membership_relationships << {
|
34
|
+
owner_class: owner_class,
|
35
|
+
owner_class_name: owner_class_name,
|
36
|
+
collection_name: collection_name,
|
37
|
+
score: score,
|
38
|
+
type: type
|
39
|
+
}
|
40
|
+
|
41
|
+
# Generate instance methods with collision-free naming
|
42
|
+
owner_class_name_lower = owner_class_name.downcase
|
43
|
+
|
44
|
+
# Method to add this object to the owner's collection
|
45
|
+
# e.g., domain.add_to_customer_domains(customer)
|
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
|
58
|
+
end
|
59
|
+
|
60
|
+
# Method to remove this object from the owner's collection
|
61
|
+
# e.g., domain.remove_from_customer_domains(customer)
|
62
|
+
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
|
73
|
+
end
|
74
|
+
|
75
|
+
# Method to check if this object is in the owner's collection
|
76
|
+
# e.g., domain.in_customer_domains?(customer)
|
77
|
+
define_method("in_#{owner_class_name_lower}_#{collection_name}?") do |owner_instance|
|
78
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
79
|
+
|
80
|
+
case type
|
81
|
+
when :sorted_set
|
82
|
+
!dbclient.zscore(collection_key, identifier).nil?
|
83
|
+
when :set
|
84
|
+
dbclient.sismember(collection_key, identifier)
|
85
|
+
when :list
|
86
|
+
dbclient.lpos(collection_key, identifier) != nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Method to get score in the owner's collection (for sorted sets)
|
91
|
+
# e.g., domain.score_in_customer_domains(customer)
|
92
|
+
if type == :sorted_set
|
93
|
+
define_method("score_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
|
94
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
95
|
+
dbclient.zscore(collection_key, identifier)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Method to get position in the owner's collection (for lists)
|
100
|
+
# e.g., domain.position_in_customer_domain_list(customer)
|
101
|
+
return unless type == :list
|
102
|
+
|
103
|
+
define_method("position_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
|
104
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
105
|
+
position = dbclient.lpos(collection_key, identifier)
|
106
|
+
position
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get all membership relationships for this class
|
111
|
+
def membership_relationships
|
112
|
+
@membership_relationships ||= []
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
# Generate collision-free instance methods for membership
|
118
|
+
def generate_membership_instance_methods(owner_class_name, collection_name, _score_calculator, type)
|
119
|
+
owner_class_name_lower = owner_class_name.downcase
|
120
|
+
|
121
|
+
# Method to add this object to the owner's collection
|
122
|
+
# e.g., domain.add_to_customer_domains(customer)
|
123
|
+
define_method("add_to_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, score = nil|
|
124
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
125
|
+
|
126
|
+
case type
|
127
|
+
when :sorted_set
|
128
|
+
# Find the owner class from the stored config
|
129
|
+
membership_config = self.class.membership_relationships.find do |config|
|
130
|
+
config[:owner_class_name] == owner_class_name && config[:collection_name] == collection_name
|
131
|
+
end
|
132
|
+
owner_class = membership_config[:owner_class] if membership_config
|
133
|
+
score ||= calculate_membership_score(owner_class, collection_name)
|
134
|
+
dbclient.zadd(collection_key, score, identifier)
|
135
|
+
when :set
|
136
|
+
dbclient.sadd(collection_key, identifier)
|
137
|
+
when :list
|
138
|
+
dbclient.lpush(collection_key, identifier)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Method to remove this object from the owner's collection
|
143
|
+
# e.g., domain.remove_from_customer_domains(customer)
|
144
|
+
define_method("remove_from_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
|
145
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
146
|
+
|
147
|
+
case type
|
148
|
+
when :sorted_set
|
149
|
+
dbclient.zrem(collection_key, identifier)
|
150
|
+
when :set
|
151
|
+
dbclient.srem(collection_key, identifier)
|
152
|
+
when :list
|
153
|
+
dbclient.lrem(collection_key, 0, identifier) # Remove all occurrences
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Method to check if this object is in the owner's collection
|
158
|
+
# e.g., domain.in_customer_domains?(customer)
|
159
|
+
define_method("in_#{owner_class_name_lower}_#{collection_name}?") do |owner_instance|
|
160
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
161
|
+
|
162
|
+
case type
|
163
|
+
when :sorted_set
|
164
|
+
dbclient.zscore(collection_key, identifier) != nil
|
165
|
+
when :set
|
166
|
+
dbclient.sismember(collection_key, identifier)
|
167
|
+
when :list
|
168
|
+
dbclient.lpos(collection_key, identifier) != nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# For sorted sets, add methods to get and update scores
|
173
|
+
if type == :sorted_set
|
174
|
+
# Method to get score in the owner's collection
|
175
|
+
# e.g., domain.score_in_customer_domains(customer)
|
176
|
+
define_method("score_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
|
177
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
178
|
+
dbclient.zscore(collection_key, identifier)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Method to update score in the owner's collection
|
182
|
+
# e.g., domain.update_score_in_customer_domains(customer, new_score)
|
183
|
+
define_method("update_score_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, new_score|
|
184
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
185
|
+
dbclient.zadd(collection_key, new_score, identifier, xx: true) # Only update existing
|
186
|
+
end
|
187
|
+
|
188
|
+
# Method to get rank in the owner's collection
|
189
|
+
# e.g., domain.rank_in_customer_domains(customer)
|
190
|
+
define_method("rank_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, reverse: false|
|
191
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
192
|
+
if reverse
|
193
|
+
dbclient.zrevrank(collection_key, identifier)
|
194
|
+
else
|
195
|
+
dbclient.zrank(collection_key, identifier)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# For lists, add position-related methods
|
201
|
+
if type == :list
|
202
|
+
# Method to get position in the owner's list
|
203
|
+
# e.g., domain.position_in_customer_domains(customer)
|
204
|
+
define_method("position_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
|
205
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
206
|
+
dbclient.lpos(collection_key, identifier)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Method to move to specific position in the owner's list
|
210
|
+
# e.g., domain.move_in_customer_domains(customer, new_position)
|
211
|
+
define_method("move_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, new_position|
|
212
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
213
|
+
|
214
|
+
# Remove and re-insert at new position
|
215
|
+
dbclient.multi do |tx|
|
216
|
+
tx.lrem(collection_key, 1, identifier)
|
217
|
+
|
218
|
+
if new_position.zero?
|
219
|
+
tx.lpush(collection_key, identifier)
|
220
|
+
elsif new_position == -1
|
221
|
+
tx.rpush(collection_key, identifier)
|
222
|
+
else
|
223
|
+
# For arbitrary positions, we need to use a more complex approach
|
224
|
+
# This is simplified - proper implementation would handle edge cases
|
225
|
+
tx.linsert(collection_key, 'BEFORE', dbclient.lindex(collection_key, new_position), identifier)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Method to get all owners that contain this object in the specified collection
|
232
|
+
# e.g., domain.all_customer_domains_owners
|
233
|
+
define_method("all_#{owner_class_name_lower}_#{collection_name}_owners") do
|
234
|
+
owners = []
|
235
|
+
pattern = "#{owner_class_name_lower}:*:#{collection_name}"
|
236
|
+
|
237
|
+
dbclient.scan_each(match: pattern) do |key|
|
238
|
+
owner_id = key.split(':')[1]
|
239
|
+
|
240
|
+
# Check if this object is in this collection
|
241
|
+
is_member = case type
|
242
|
+
when :sorted_set
|
243
|
+
dbclient.zscore(key, identifier) != nil
|
244
|
+
when :set
|
245
|
+
dbclient.sismember(key, identifier)
|
246
|
+
when :list
|
247
|
+
dbclient.lpos(key, identifier) != nil
|
248
|
+
end
|
249
|
+
|
250
|
+
if is_member
|
251
|
+
# Try to instantiate the owner object
|
252
|
+
begin
|
253
|
+
owners << owner_class.new(identifier: owner_id)
|
254
|
+
rescue NameError
|
255
|
+
# Owner class not available, just store the ID
|
256
|
+
owners << { class: owner_class_name, id: owner_id }
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
owners
|
262
|
+
end
|
263
|
+
|
264
|
+
# Batch method to add to multiple owners' collections at once
|
265
|
+
# e.g., domain.add_to_multiple_customer_domains([customer1, customer2])
|
266
|
+
define_method("add_to_multiple_#{owner_class_name_lower}_#{collection_name}") do |owner_instances, score = nil|
|
267
|
+
return if owner_instances.empty?
|
268
|
+
|
269
|
+
dbclient.pipelined do |pipeline|
|
270
|
+
owner_instances.each do |owner_instance|
|
271
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
272
|
+
|
273
|
+
case type
|
274
|
+
when :sorted_set
|
275
|
+
# Find the owner class from the stored config
|
276
|
+
membership_config = self.class.membership_relationships.find do |config|
|
277
|
+
config[:owner_class_name] == owner_class_name && config[:collection_name] == collection_name
|
278
|
+
end
|
279
|
+
owner_class = membership_config[:owner_class] if membership_config
|
280
|
+
calculated_score = score || calculate_membership_score(owner_class, collection_name)
|
281
|
+
pipeline.zadd(collection_key, calculated_score, identifier)
|
282
|
+
when :set
|
283
|
+
pipeline.sadd(collection_key, identifier)
|
284
|
+
when :list
|
285
|
+
pipeline.lpush(collection_key, identifier)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Batch method to remove from multiple owners' collections at once
|
292
|
+
# e.g., domain.remove_from_multiple_customer_domains([customer1, customer2])
|
293
|
+
define_method("remove_from_multiple_#{owner_class_name_lower}_#{collection_name}") do |owner_instances|
|
294
|
+
return if owner_instances.empty?
|
295
|
+
|
296
|
+
dbclient.pipelined do |pipeline|
|
297
|
+
owner_instances.each do |owner_instance|
|
298
|
+
collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
|
299
|
+
|
300
|
+
case type
|
301
|
+
when :sorted_set
|
302
|
+
pipeline.zrem(collection_key, identifier)
|
303
|
+
when :set
|
304
|
+
pipeline.srem(collection_key, identifier)
|
305
|
+
when :list
|
306
|
+
pipeline.lrem(collection_key, 0, identifier)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Instance methods for objects with membership relationships
|
315
|
+
module InstanceMethods
|
316
|
+
# Calculate the appropriate score for a membership relationship
|
317
|
+
#
|
318
|
+
# @param owner_class [Class] The owner class (e.g., Customer)
|
319
|
+
# @param collection_name [Symbol] The collection name (e.g., :domains)
|
320
|
+
# @return [Float] Calculated score
|
321
|
+
def calculate_membership_score(owner_class, collection_name)
|
322
|
+
# Find the membership configuration
|
323
|
+
membership_config = self.class.membership_relationships.find do |config|
|
324
|
+
config[:owner_class] == owner_class && config[:collection_name] == collection_name
|
325
|
+
end
|
326
|
+
|
327
|
+
return default_score unless membership_config
|
328
|
+
|
329
|
+
score_calculator = membership_config[:score]
|
330
|
+
|
331
|
+
# Extract the score calculation logic to reduce complexity
|
332
|
+
calculated_score = extract_score_from_calculator(score_calculator)
|
333
|
+
calculated_score || default_score
|
334
|
+
end
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
def extract_score_from_calculator(score_calculator)
|
339
|
+
case score_calculator
|
340
|
+
when Symbol
|
341
|
+
extract_score_from_symbol(score_calculator)
|
342
|
+
when Proc
|
343
|
+
extract_score_from_proc(score_calculator)
|
344
|
+
when Numeric
|
345
|
+
score_calculator.to_f
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def extract_score_from_symbol(symbol)
|
350
|
+
return nil unless respond_to?(symbol)
|
351
|
+
|
352
|
+
value = send(symbol)
|
353
|
+
if value.respond_to?(:to_f)
|
354
|
+
value.to_f
|
355
|
+
elsif value.respond_to?(:to_i)
|
356
|
+
encode_score(value, 0)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def extract_score_from_proc(proc)
|
361
|
+
result = instance_exec(&proc)
|
362
|
+
return nil if result.nil?
|
363
|
+
|
364
|
+
result.respond_to?(:to_f) ? result.to_f : nil
|
365
|
+
end
|
366
|
+
|
367
|
+
def default_score
|
368
|
+
respond_to?(:current_score) ? current_score : Time.now.to_f
|
369
|
+
end
|
370
|
+
|
371
|
+
# Update membership in all collections atomically
|
372
|
+
def update_all_memberships(_action = :add)
|
373
|
+
nil unless self.class.respond_to?(:membership_relationships)
|
374
|
+
|
375
|
+
# This is a simplified version - in practice, you'd need to know
|
376
|
+
# which specific owner instances this object should be a member of
|
377
|
+
# For now, we'll skip the automatic update and rely on explicit calls
|
378
|
+
end
|
379
|
+
|
380
|
+
# Remove from all membership collections (used during destroy)
|
381
|
+
def remove_from_all_memberships
|
382
|
+
return unless self.class.respond_to?(:membership_relationships)
|
383
|
+
|
384
|
+
self.class.membership_relationships.each do |config|
|
385
|
+
owner_class_name = config[:owner_class_name]
|
386
|
+
collection_name = config[:collection_name]
|
387
|
+
type = config[:type]
|
388
|
+
|
389
|
+
# Find all collections this object is a member of
|
390
|
+
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
391
|
+
|
392
|
+
dbclient.scan_each(match: pattern) do |key|
|
393
|
+
case type
|
394
|
+
when :sorted_set
|
395
|
+
dbclient.zrem(key, identifier)
|
396
|
+
when :set
|
397
|
+
dbclient.srem(key, identifier)
|
398
|
+
when :list
|
399
|
+
dbclient.lrem(key, 0, identifier)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
# Get all memberships this object has
|
406
|
+
#
|
407
|
+
# @return [Array<Hash>] Array of membership information
|
408
|
+
def membership_collections
|
409
|
+
return [] unless self.class.respond_to?(:membership_relationships)
|
410
|
+
|
411
|
+
memberships = []
|
412
|
+
|
413
|
+
self.class.membership_relationships.each do |config|
|
414
|
+
owner_class_name = config[:owner_class_name]
|
415
|
+
collection_name = config[:collection_name]
|
416
|
+
type = config[:type]
|
417
|
+
|
418
|
+
# Find all collections this object is a member of
|
419
|
+
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
420
|
+
|
421
|
+
dbclient.scan_each(match: pattern) do |key|
|
422
|
+
is_member = case type
|
423
|
+
when :sorted_set
|
424
|
+
score = dbclient.zscore(key, identifier)
|
425
|
+
next unless score
|
426
|
+
|
427
|
+
{ score: score, decoded_score: decode_score(score) }
|
428
|
+
when :set
|
429
|
+
next unless dbclient.sismember(key, identifier)
|
430
|
+
|
431
|
+
{}
|
432
|
+
when :list
|
433
|
+
position = dbclient.lpos(key, identifier)
|
434
|
+
next unless position
|
435
|
+
|
436
|
+
{ position: position }
|
437
|
+
end
|
438
|
+
|
439
|
+
if is_member
|
440
|
+
owner_id = key.split(':')[1]
|
441
|
+
memberships << {
|
442
|
+
owner_class: owner_class_name,
|
443
|
+
owner_id: owner_id,
|
444
|
+
collection_name: collection_name,
|
445
|
+
type: type,
|
446
|
+
key: key
|
447
|
+
}.merge(is_member)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
memberships
|
453
|
+
end
|
454
|
+
|
455
|
+
# Transfer membership from one owner to another
|
456
|
+
#
|
457
|
+
# @param from_owner [Object] Source owner instance
|
458
|
+
# @param to_owner [Object] Target owner instance
|
459
|
+
# @param collection_name [Symbol] Collection to transfer membership in
|
460
|
+
def transfer_membership(from_owner, to_owner, collection_name)
|
461
|
+
# Find the membership configuration
|
462
|
+
config = self.class.membership_relationships.find do |rel|
|
463
|
+
rel[:collection_name] == collection_name &&
|
464
|
+
(rel[:owner_class] == from_owner.class || rel[:owner_class_name] == from_owner.class.name)
|
465
|
+
end
|
466
|
+
|
467
|
+
return false unless config
|
468
|
+
|
469
|
+
owner_class_name = config[:owner_class_name].downcase
|
470
|
+
type = config[:type]
|
471
|
+
|
472
|
+
from_key = "#{owner_class_name}:#{from_owner.identifier}:#{collection_name}"
|
473
|
+
to_key = "#{owner_class_name}:#{to_owner.identifier}:#{collection_name}"
|
474
|
+
|
475
|
+
dbclient.multi do |tx|
|
476
|
+
case type
|
477
|
+
when :sorted_set
|
478
|
+
score = dbclient.zscore(from_key, identifier)
|
479
|
+
if score
|
480
|
+
tx.zrem(from_key, identifier)
|
481
|
+
tx.zadd(to_key, score, identifier)
|
482
|
+
end
|
483
|
+
when :set
|
484
|
+
if dbclient.sismember(from_key, identifier)
|
485
|
+
tx.srem(from_key, identifier)
|
486
|
+
tx.sadd(to_key, identifier)
|
487
|
+
end
|
488
|
+
when :list
|
489
|
+
if dbclient.lpos(from_key, identifier)
|
490
|
+
tx.lrem(from_key, 1, identifier)
|
491
|
+
tx.lpush(to_key, identifier)
|
492
|
+
end
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
true
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|