familia 2.0.0.pre15 → 2.0.0.pre17
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 +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -1,274 +0,0 @@
|
|
1
|
-
# lib/familia/features/relationships/redis_operations.rb
|
2
|
-
|
3
|
-
module Familia
|
4
|
-
module Features
|
5
|
-
module Relationships
|
6
|
-
# Redis operations module providing atomic multi-collection operations
|
7
|
-
# and native Redis set operations for relationships
|
8
|
-
module RedisOperations
|
9
|
-
# Execute multiple Redis operations atomically using MULTI/EXEC
|
10
|
-
#
|
11
|
-
# @param redis [Redis] Redis connection to use
|
12
|
-
# @yield [Redis] Yields Redis connection in transaction context
|
13
|
-
# @return [Array] Results from Redis transaction
|
14
|
-
#
|
15
|
-
# @example Atomic multi-collection update
|
16
|
-
# atomic_operation(redis) do |tx|
|
17
|
-
# tx.zadd("customer:123:domains", score, domain_id)
|
18
|
-
# tx.zadd("team:456:domains", score, domain_id)
|
19
|
-
# tx.hset("domain_index", domain_name, domain_id)
|
20
|
-
# end
|
21
|
-
def atomic_operation(redis = nil)
|
22
|
-
redis ||= redis_connection
|
23
|
-
|
24
|
-
redis.multi do |tx|
|
25
|
-
yield tx if block_given?
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# Update object presence in multiple collections atomically
|
30
|
-
#
|
31
|
-
# @param collections [Array<Hash>] Array of collection configurations
|
32
|
-
# @param action [Symbol] Action to perform (:add, :remove)
|
33
|
-
# @param identifier [String] Object identifier
|
34
|
-
# @param default_score [Float] Default score if not specified per collection
|
35
|
-
#
|
36
|
-
# @example Update presence in multiple collections
|
37
|
-
# update_multiple_presence([
|
38
|
-
# { key: "customer:123:domains", score: current_score },
|
39
|
-
# { key: "team:456:domains", score: permission_encode(Time.now, :read) },
|
40
|
-
# { key: "org:789:all_domains", score: current_score }
|
41
|
-
# ], :add, domain.identifier)
|
42
|
-
def update_multiple_presence(collections, action, identifier, default_score = nil)
|
43
|
-
return unless collections&.any?
|
44
|
-
|
45
|
-
redis = self.class.dbclient
|
46
|
-
|
47
|
-
atomic_operation(redis) do |tx|
|
48
|
-
collections.each do |collection_config|
|
49
|
-
redis_key = collection_config[:key]
|
50
|
-
score = collection_config[:score] || default_score || current_score
|
51
|
-
|
52
|
-
case action
|
53
|
-
when :add
|
54
|
-
tx.zadd(redis_key, score, identifier)
|
55
|
-
when :remove
|
56
|
-
tx.zrem(redis_key, identifier)
|
57
|
-
when :update
|
58
|
-
# Use ZADD with XX flag to only update existing members
|
59
|
-
tx.zadd(redis_key, score, identifier, xx: true)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
# Perform Redis set operations (union, intersection, difference) on sorted sets
|
66
|
-
#
|
67
|
-
# @param operation [Symbol] Operation type (:union, :intersection, :difference)
|
68
|
-
# @param destination [String] Redis key for result storage
|
69
|
-
# @param source_keys [Array<String>] Source Redis keys to operate on
|
70
|
-
# @param weights [Array<Float>] Optional weights for union operations
|
71
|
-
# @param aggregate [Symbol] Aggregation method (:sum, :min, :max)
|
72
|
-
# @param ttl [Integer] TTL for destination key in seconds
|
73
|
-
# @return [Integer] Number of elements in resulting set
|
74
|
-
#
|
75
|
-
# @example Union of accessible domains
|
76
|
-
# set_operation(:union, "temp:accessible_domains:#{user_id}",
|
77
|
-
# ["customer:domains", "team:domains", "org:domains"],
|
78
|
-
# ttl: 300)
|
79
|
-
def set_operation(operation, destination, source_keys, weights: nil, aggregate: :sum, ttl: nil)
|
80
|
-
return 0 if source_keys.empty?
|
81
|
-
|
82
|
-
redis = redis_connection
|
83
|
-
|
84
|
-
atomic_operation(redis) do |tx|
|
85
|
-
case operation
|
86
|
-
when :union
|
87
|
-
if weights
|
88
|
-
tx.zunionstore(destination, source_keys.zip(weights).to_h, aggregate: aggregate)
|
89
|
-
else
|
90
|
-
tx.zunionstore(destination, source_keys, aggregate: aggregate)
|
91
|
-
end
|
92
|
-
when :intersection
|
93
|
-
if weights
|
94
|
-
tx.zinterstore(destination, source_keys.zip(weights).to_h, aggregate: aggregate)
|
95
|
-
else
|
96
|
-
tx.zinterstore(destination, source_keys, aggregate: aggregate)
|
97
|
-
end
|
98
|
-
when :difference
|
99
|
-
first_key = source_keys.first
|
100
|
-
other_keys = source_keys[1..] || []
|
101
|
-
|
102
|
-
tx.zunionstore(destination, [first_key])
|
103
|
-
other_keys.each do |key|
|
104
|
-
members = redis.zrange(key, 0, -1)
|
105
|
-
tx.zrem(destination, members) if members.any?
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
tx.expire(destination, ttl) if ttl
|
110
|
-
end
|
111
|
-
|
112
|
-
redis.zcard(destination)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Create temporary Redis key with automatic cleanup
|
116
|
-
#
|
117
|
-
# @param base_name [String] Base name for the temporary key
|
118
|
-
# @param ttl [Integer] TTL in seconds (default: 300)
|
119
|
-
# @return [String] Generated temporary key name
|
120
|
-
#
|
121
|
-
# @example
|
122
|
-
# temp_key = create_temp_key("user_accessible_domains", 600)
|
123
|
-
# #=> "temp:user_accessible_domains:1704067200:abc123"
|
124
|
-
def create_temp_key(base_name, ttl = 300)
|
125
|
-
timestamp = Time.now.to_i
|
126
|
-
random_suffix = SecureRandom.hex(3)
|
127
|
-
temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
|
128
|
-
|
129
|
-
# Set immediate expiry to ensure cleanup even if operation fails
|
130
|
-
redis_connection.expire(temp_key, ttl)
|
131
|
-
|
132
|
-
temp_key
|
133
|
-
end
|
134
|
-
|
135
|
-
# Batch add multiple items to a sorted set
|
136
|
-
#
|
137
|
-
# @param redis_key [String] Redis sorted set key
|
138
|
-
# @param items [Array<Hash>] Array of {member: String, score: Float} hashes
|
139
|
-
# @param mode [Symbol] Add mode (:normal, :nx, :xx, :lt, :gt)
|
140
|
-
#
|
141
|
-
# @example Batch add domains with scores
|
142
|
-
# batch_zadd("customer:domains", [
|
143
|
-
# { member: "domain1", score: encode_score(Time.now, permission: :read) },
|
144
|
-
# { member: "domain2", score: encode_score(Time.now, permission: :write) }
|
145
|
-
# ])
|
146
|
-
def batch_zadd(redis_key, items, mode: :normal)
|
147
|
-
return 0 if items.empty?
|
148
|
-
|
149
|
-
redis = redis_connection
|
150
|
-
zadd_args = items.flat_map { |item| [item[:score], item[:member]] }
|
151
|
-
|
152
|
-
case mode
|
153
|
-
when :nx
|
154
|
-
redis.zadd(redis_key, zadd_args, nx: true)
|
155
|
-
when :xx
|
156
|
-
redis.zadd(redis_key, zadd_args, xx: true)
|
157
|
-
when :lt
|
158
|
-
redis.zadd(redis_key, zadd_args, lt: true)
|
159
|
-
when :gt
|
160
|
-
redis.zadd(redis_key, zadd_args, gt: true)
|
161
|
-
else
|
162
|
-
redis.zadd(redis_key, zadd_args)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
# Query sorted set with score filtering and permission checking
|
167
|
-
#
|
168
|
-
# @param redis_key [String] Redis sorted set key
|
169
|
-
# @param start_score [Float] Minimum score (inclusive)
|
170
|
-
# @param end_score [Float] Maximum score (inclusive)
|
171
|
-
# @param offset [Integer] Offset for pagination
|
172
|
-
# @param count [Integer] Maximum number of results
|
173
|
-
# @param with_scores [Boolean] Include scores in results
|
174
|
-
# @param min_permission [Symbol] Minimum permission level required
|
175
|
-
# @return [Array] Query results
|
176
|
-
#
|
177
|
-
# @example Query domains with read permission or higher
|
178
|
-
# query_by_score("customer:domains",
|
179
|
-
# encode_score(1.hour.ago, 0),
|
180
|
-
# encode_score(Time.now, MAX_METADATA),
|
181
|
-
# min_permission: :read)
|
182
|
-
def query_by_score(redis_key, start_score = '-inf', end_score = '+inf',
|
183
|
-
offset: 0, count: -1, with_scores: false, min_permission: nil)
|
184
|
-
self.class.dbclient
|
185
|
-
|
186
|
-
# Adjust score range for permission filtering
|
187
|
-
if min_permission
|
188
|
-
permission_value = ScoreEncoding.permission_level_value(min_permission)
|
189
|
-
# Ensure minimum score includes required permission level
|
190
|
-
if start_score.is_a?(Numeric)
|
191
|
-
decoded = decode_score(start_score)
|
192
|
-
if decoded[:permissions] < permission_value
|
193
|
-
start_score = encode_score(decoded[:timestamp],
|
194
|
-
permission_value)
|
195
|
-
end
|
196
|
-
else
|
197
|
-
start_score = encode_score(0, permission_value)
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
options = {
|
202
|
-
limit: (count.positive? ? [offset, count] : nil),
|
203
|
-
with_scores: with_scores
|
204
|
-
}.compact
|
205
|
-
|
206
|
-
results = dbclient.zrangebyscore(redis_key, start_score, end_score, **options)
|
207
|
-
|
208
|
-
# Filter results by permission if needed using correct bitwise operations
|
209
|
-
if min_permission && with_scores
|
210
|
-
permission_mask = ScoreEncoding.permission_level_value(min_permission)
|
211
|
-
results = results.select do |_member, score|
|
212
|
-
decoded = decode_score(score)
|
213
|
-
# Use bitwise AND to check if permission mask is satisfied
|
214
|
-
decoded[:permissions].allbits?(permission_mask)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
results
|
219
|
-
end
|
220
|
-
|
221
|
-
# Clean up expired temporary keys
|
222
|
-
#
|
223
|
-
# @param pattern [String] Pattern to match temporary keys
|
224
|
-
# @param batch_size [Integer] Number of keys to process at once
|
225
|
-
#
|
226
|
-
# @example Clean up old temporary keys
|
227
|
-
# cleanup_temp_keys("temp:user_*", 100)
|
228
|
-
def cleanup_temp_keys(pattern = 'temp:*', batch_size = 100)
|
229
|
-
self.class.dbclient
|
230
|
-
cursor = 0
|
231
|
-
|
232
|
-
loop do
|
233
|
-
cursor, keys = dbclient.scan(cursor, match: pattern, count: batch_size)
|
234
|
-
|
235
|
-
if keys.any?
|
236
|
-
# Check TTL and remove keys that should have expired
|
237
|
-
keys.each_slice(batch_size) do |key_batch|
|
238
|
-
dbclient.pipelined do |pipeline|
|
239
|
-
key_batch.each do |key|
|
240
|
-
ttl = dbclient.ttl(key)
|
241
|
-
pipeline.del(key) if ttl == -1 # Key exists but has no TTL
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
break if cursor.zero?
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
# Get Redis connection for the current class or instance
|
252
|
-
def redis_connection
|
253
|
-
if self.class.respond_to?(:dbclient)
|
254
|
-
self.class.dbclient
|
255
|
-
elsif respond_to?(:dbclient)
|
256
|
-
dbclient
|
257
|
-
else
|
258
|
-
Familia.dbclient
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
262
|
-
private
|
263
|
-
|
264
|
-
# Validate Redis key format
|
265
|
-
def validate_redis_key(key)
|
266
|
-
raise ArgumentError, 'Redis key cannot be nil or empty' if key.nil? || key.empty?
|
267
|
-
raise ArgumentError, 'Redis key must be a string' unless key.is_a?(String)
|
268
|
-
|
269
|
-
key
|
270
|
-
end
|
271
|
-
end
|
272
|
-
end
|
273
|
-
end
|
274
|
-
end
|
@@ -1,418 +0,0 @@
|
|
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 class-level tracked collection
|
38
|
-
#
|
39
|
-
# @param collection_name [Symbol] Name of the class-level collection
|
40
|
-
# @param score [Symbol, Proc, nil] How to calculate the score
|
41
|
-
# @param on_destroy [Symbol] What to do when object is destroyed (:remove, :ignore)
|
42
|
-
#
|
43
|
-
# @example Class-level tracking (using class_ prefix convention)
|
44
|
-
# class_tracked_in :all_customers, score: :created_at
|
45
|
-
# class_tracked_in :active_users, score: -> { status == 'active' ? Time.now.to_i : 0 }
|
46
|
-
def class_tracked_in(collection_name, score: nil, on_destroy: :remove)
|
47
|
-
|
48
|
-
klass_name = (name || self.to_s).downcase
|
49
|
-
|
50
|
-
# Store metadata for this tracking relationship
|
51
|
-
tracking_relationships << {
|
52
|
-
context_class: klass_name,
|
53
|
-
context_class_name: name || self.to_s,
|
54
|
-
collection_name: collection_name,
|
55
|
-
score: score,
|
56
|
-
on_destroy: on_destroy
|
57
|
-
}
|
58
|
-
|
59
|
-
# Generate class-level collection methods
|
60
|
-
generate_tracking_class_methods(self, collection_name)
|
61
|
-
|
62
|
-
# Generate instance methods for class-level tracking
|
63
|
-
generate_tracking_instance_methods('class', collection_name, score)
|
64
|
-
end
|
65
|
-
|
66
|
-
# Define a tracked_in relationship
|
67
|
-
#
|
68
|
-
# @param context_class [Class, Symbol] The class that owns the collection
|
69
|
-
# @param collection_name [Symbol] Name of the collection
|
70
|
-
# @param score [Symbol, Proc, nil] How to calculate the score
|
71
|
-
# @param on_destroy [Symbol] What to do when object is destroyed (:remove, :ignore)
|
72
|
-
#
|
73
|
-
# @example Basic tracking
|
74
|
-
# tracked_in Customer, :domains, score: :created_at
|
75
|
-
#
|
76
|
-
# @example Multi-presence tracking
|
77
|
-
# tracked_in Customer, :domains, score: -> { permission_encode(created_at, permission_level) }
|
78
|
-
# tracked_in Team, :domains, score: :added_at
|
79
|
-
# tracked_in Organization, :all_domains, score: :created_at
|
80
|
-
def tracked_in(context_class, collection_name, score: nil, on_destroy: :remove)
|
81
|
-
|
82
|
-
# Handle class context
|
83
|
-
if context_class.is_a?(Class)
|
84
|
-
class_name = context_class.name
|
85
|
-
context_class_name = if class_name.include?('::')
|
86
|
-
# Extract the last part after the last ::
|
87
|
-
class_name.split('::').last
|
88
|
-
else
|
89
|
-
class_name
|
90
|
-
end
|
91
|
-
else
|
92
|
-
context_class_name = camelize_word(context_class)
|
93
|
-
end
|
94
|
-
|
95
|
-
# Store metadata for this tracking relationship
|
96
|
-
tracking_relationships << {
|
97
|
-
context_class: context_class,
|
98
|
-
context_class_name: context_class_name,
|
99
|
-
collection_name: collection_name,
|
100
|
-
score: score,
|
101
|
-
on_destroy: on_destroy
|
102
|
-
}
|
103
|
-
|
104
|
-
# Generate context class methods
|
105
|
-
generate_context_class_methods(context_class, collection_name)
|
106
|
-
|
107
|
-
# Generate instance methods on this class
|
108
|
-
generate_tracking_instance_methods(context_class_name, collection_name, score)
|
109
|
-
end
|
110
|
-
|
111
|
-
# Get all tracking relationships for this class
|
112
|
-
def tracking_relationships
|
113
|
-
@tracking_relationships ||= []
|
114
|
-
end
|
115
|
-
|
116
|
-
private
|
117
|
-
|
118
|
-
# Generate class-level collection methods (e.g., User.all_users)
|
119
|
-
def generate_tracking_class_methods(target_class, collection_name)
|
120
|
-
# Generate class-level collection getter method
|
121
|
-
target_class.define_singleton_method("#{collection_name}") do
|
122
|
-
collection_key = "#{self.name.downcase}:#{collection_name}"
|
123
|
-
Familia::SortedSet.new(nil, dbkey: collection_key, logical_database: logical_database)
|
124
|
-
end
|
125
|
-
|
126
|
-
# Generate class-level add method (e.g., User.add_to_all_users)
|
127
|
-
target_class.define_singleton_method("add_to_#{collection_name}") do |item, score = nil|
|
128
|
-
collection = send("#{collection_name}")
|
129
|
-
|
130
|
-
# Calculate score if not provided
|
131
|
-
score ||= if item.respond_to?(:calculate_tracking_score)
|
132
|
-
item.calculate_tracking_score('class', collection_name)
|
133
|
-
else
|
134
|
-
item.current_score
|
135
|
-
end
|
136
|
-
|
137
|
-
# Ensure score is never nil
|
138
|
-
score = item.current_score if score.nil?
|
139
|
-
|
140
|
-
collection.add(score, item.identifier)
|
141
|
-
end
|
142
|
-
|
143
|
-
# Generate class-level remove method
|
144
|
-
target_class.define_singleton_method("remove_from_#{collection_name}") do |item|
|
145
|
-
collection = send("#{collection_name}")
|
146
|
-
collection.delete(item.identifier)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
# Generate methods on the context class (e.g., Customer.domains)
|
151
|
-
def generate_context_class_methods(context_class, collection_name)
|
152
|
-
# Resolve context class if it's a symbol/string
|
153
|
-
actual_context_class = context_class.is_a?(Class) ? context_class : Object.const_get(camelize_word(context_class))
|
154
|
-
|
155
|
-
# Generate collection getter method
|
156
|
-
actual_context_class.define_method(collection_name) do
|
157
|
-
collection_key = "#{self.class.name.downcase}:#{identifier}:#{collection_name}"
|
158
|
-
Familia::SortedSet.new(nil, dbkey: collection_key, logical_database: self.class.logical_database)
|
159
|
-
end
|
160
|
-
|
161
|
-
# Generate add method (e.g., Customer#add_domain)
|
162
|
-
actual_context_class.define_method("add_#{singularize_word(collection_name)}") do |item, score = nil|
|
163
|
-
collection = send(collection_name)
|
164
|
-
|
165
|
-
# Calculate score if not provided
|
166
|
-
score ||= if item.respond_to?(:calculate_tracking_score)
|
167
|
-
item.calculate_tracking_score(self.class, collection_name)
|
168
|
-
else
|
169
|
-
item.current_score
|
170
|
-
end
|
171
|
-
|
172
|
-
# Ensure score is never nil
|
173
|
-
score = item.current_score if score.nil?
|
174
|
-
|
175
|
-
collection.add(score, item.identifier)
|
176
|
-
end
|
177
|
-
|
178
|
-
# Generate remove method (e.g., Customer#remove_domain)
|
179
|
-
actual_context_class.define_method("remove_#{singularize_word(collection_name)}") do |item|
|
180
|
-
collection = send(collection_name)
|
181
|
-
collection.delete(item.identifier)
|
182
|
-
end
|
183
|
-
|
184
|
-
# Generate bulk add method (e.g., Customer#add_domains)
|
185
|
-
actual_context_class.define_method("add_#{collection_name}") do |items|
|
186
|
-
return if items.empty?
|
187
|
-
|
188
|
-
collection = send(collection_name)
|
189
|
-
|
190
|
-
# Prepare batch data
|
191
|
-
batch_data = items.map do |item|
|
192
|
-
score = if item.respond_to?(:calculate_tracking_score)
|
193
|
-
item.calculate_tracking_score(self.class, collection_name)
|
194
|
-
else
|
195
|
-
item.current_score
|
196
|
-
end
|
197
|
-
# Ensure score is never nil
|
198
|
-
score = item.current_score if score.nil?
|
199
|
-
{ member: item.identifier, score: score }
|
200
|
-
end
|
201
|
-
|
202
|
-
# Use batch operation from RedisOperations
|
203
|
-
collection.dbclient.pipelined do |pipeline|
|
204
|
-
batch_data.each do |data|
|
205
|
-
pipeline.zadd(collection.rediskey, data[:score], data[:member])
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
# Generate query methods with score filtering
|
211
|
-
actual_context_class.define_method("#{collection_name}_with_permission") do |min_permission = :read|
|
212
|
-
collection = send(collection_name)
|
213
|
-
permission_score = ScoreEncoding.permission_encode(0, min_permission)
|
214
|
-
|
215
|
-
collection.zrangebyscore(permission_score, '+inf', with_scores: true)
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
# Generate instance methods on the tracked class
|
220
|
-
def generate_tracking_instance_methods(context_class_name, collection_name, _score_calculator)
|
221
|
-
# Method to check if this object is in a specific collection
|
222
|
-
# e.g., domain.in_customer_domains?(customer)
|
223
|
-
define_method("in_#{context_class_name.downcase}_#{collection_name}?") do |context_instance|
|
224
|
-
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
225
|
-
dbclient.zscore(collection_key, identifier) != nil
|
226
|
-
end
|
227
|
-
|
228
|
-
# Method to add this object to a specific collection
|
229
|
-
# e.g., domain.add_to_customer_domains(customer, score)
|
230
|
-
define_method("add_to_#{context_class_name.downcase}_#{collection_name}") do |context_instance, score = nil|
|
231
|
-
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
232
|
-
|
233
|
-
score ||= calculate_tracking_score(context_class_name, collection_name)
|
234
|
-
|
235
|
-
# Ensure score is never nil
|
236
|
-
score = current_score if score.nil?
|
237
|
-
|
238
|
-
dbclient.zadd(collection_key, score, identifier)
|
239
|
-
end
|
240
|
-
|
241
|
-
# Method to remove this object from a specific collection
|
242
|
-
# e.g., domain.remove_from_customer_domains(customer)
|
243
|
-
define_method("remove_from_#{context_class_name.downcase}_#{collection_name}") do |context_instance|
|
244
|
-
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
245
|
-
dbclient.zrem(collection_key, identifier)
|
246
|
-
end
|
247
|
-
|
248
|
-
# Method to get score in a specific collection
|
249
|
-
# e.g., domain.score_in_customer_domains(customer)
|
250
|
-
define_method("score_in_#{context_class_name.downcase}_#{collection_name}") do |context_instance|
|
251
|
-
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
252
|
-
dbclient.zscore(collection_key, identifier)
|
253
|
-
end
|
254
|
-
|
255
|
-
# Method to update score in a specific collection
|
256
|
-
# e.g., domain.update_score_in_customer_domains(customer, new_score)
|
257
|
-
define_method("update_score_in_#{context_class_name.downcase}_#{collection_name}") do |context_instance, new_score|
|
258
|
-
collection_key = "#{context_class_name.downcase}:#{context_instance.identifier}:#{collection_name}"
|
259
|
-
dbclient.zadd(collection_key, new_score, identifier, xx: true) # Only update existing
|
260
|
-
end
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
# Instance methods for tracked objects
|
265
|
-
module InstanceMethods
|
266
|
-
# Calculate the appropriate score for a tracking relationship
|
267
|
-
#
|
268
|
-
# @param context_class [Class] The context class (e.g., Customer)
|
269
|
-
# @param collection_name [Symbol] The collection name (e.g., :domains)
|
270
|
-
# @return [Float] Calculated score
|
271
|
-
def calculate_tracking_score(context_class, collection_name)
|
272
|
-
# Find the tracking configuration
|
273
|
-
tracking_config = self.class.tracking_relationships.find do |config|
|
274
|
-
config[:context_class] == context_class && config[:collection_name] == collection_name
|
275
|
-
end
|
276
|
-
|
277
|
-
return current_score unless tracking_config
|
278
|
-
|
279
|
-
score_calculator = tracking_config[:score]
|
280
|
-
|
281
|
-
case score_calculator
|
282
|
-
when Symbol
|
283
|
-
# Field name or method name
|
284
|
-
if respond_to?(score_calculator)
|
285
|
-
value = send(score_calculator)
|
286
|
-
if value.respond_to?(:to_f)
|
287
|
-
value.to_f
|
288
|
-
elsif value.respond_to?(:to_i)
|
289
|
-
encode_score(value, 0)
|
290
|
-
else
|
291
|
-
current_score
|
292
|
-
end
|
293
|
-
else
|
294
|
-
current_score
|
295
|
-
end
|
296
|
-
when Proc
|
297
|
-
# Execute proc in context of this instance
|
298
|
-
result = instance_exec(&score_calculator)
|
299
|
-
# Ensure we get a numeric result
|
300
|
-
if result.nil?
|
301
|
-
current_score
|
302
|
-
elsif result.respond_to?(:to_f)
|
303
|
-
result.to_f
|
304
|
-
else
|
305
|
-
current_score
|
306
|
-
end
|
307
|
-
when Numeric
|
308
|
-
score_calculator.to_f
|
309
|
-
else
|
310
|
-
current_score
|
311
|
-
end
|
312
|
-
end
|
313
|
-
|
314
|
-
# Update presence in all tracked collections atomically
|
315
|
-
def update_all_tracking_collections
|
316
|
-
return unless self.class.respond_to?(:tracking_relationships)
|
317
|
-
|
318
|
-
[]
|
319
|
-
|
320
|
-
self.class.tracking_relationships.each do |config|
|
321
|
-
config[:context_class_name]
|
322
|
-
config[:collection_name]
|
323
|
-
|
324
|
-
# This is a simplified version - in practice, you'd need to know
|
325
|
-
# which specific instances this object should be tracked in
|
326
|
-
# For now, we'll skip the automatic update and rely on explicit calls
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
# Add to class-level tracking collections automatically
|
331
|
-
def add_to_class_tracking_collections
|
332
|
-
return unless self.class.respond_to?(:tracking_relationships)
|
333
|
-
|
334
|
-
self.class.tracking_relationships.each do |config|
|
335
|
-
context_class_name = config[:context_class_name]
|
336
|
-
context_class = config[:context_class]
|
337
|
-
collection_name = config[:collection_name]
|
338
|
-
|
339
|
-
# Only auto-add to class-level collections (where context_class matches self.class)
|
340
|
-
if context_class_name.downcase == self.class.name.downcase
|
341
|
-
# Call the class method to add this object
|
342
|
-
self.class.send("add_to_#{collection_name}", self)
|
343
|
-
end
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
# Remove from all tracking collections (used during destroy)
|
348
|
-
def remove_from_all_tracking_collections
|
349
|
-
return unless self.class.respond_to?(:tracking_relationships)
|
350
|
-
|
351
|
-
# Get all possible collection keys this object might be in
|
352
|
-
# This is expensive but necessary for cleanup
|
353
|
-
redis_conn = redis
|
354
|
-
pattern = '*:*:*' # This could be optimized with better key patterns
|
355
|
-
|
356
|
-
cursor = 0
|
357
|
-
matching_keys = []
|
358
|
-
|
359
|
-
loop do
|
360
|
-
cursor, keys = redis_conn.scan(cursor, match: pattern, count: 1000)
|
361
|
-
matching_keys.concat(keys)
|
362
|
-
break if cursor.zero?
|
363
|
-
end
|
364
|
-
|
365
|
-
# Filter keys that might contain this object and remove it
|
366
|
-
redis_conn.pipelined do |pipeline|
|
367
|
-
matching_keys.each do |key|
|
368
|
-
# Check if this key matches any of our tracking relationships
|
369
|
-
self.class.tracking_relationships.each do |config|
|
370
|
-
context_class_name = config[:context_class_name].downcase
|
371
|
-
collection_name = config[:collection_name]
|
372
|
-
|
373
|
-
if key.include?(context_class_name) && key.include?(collection_name.to_s)
|
374
|
-
pipeline.zrem(key, identifier)
|
375
|
-
end
|
376
|
-
end
|
377
|
-
end
|
378
|
-
end
|
379
|
-
end
|
380
|
-
|
381
|
-
# Get all collections this object appears in
|
382
|
-
#
|
383
|
-
# @return [Array<Hash>] Array of collection information
|
384
|
-
def tracking_collections_membership
|
385
|
-
return [] unless self.class.respond_to?(:tracking_relationships)
|
386
|
-
|
387
|
-
memberships = []
|
388
|
-
|
389
|
-
self.class.tracking_relationships.each do |config|
|
390
|
-
context_class_name = config[:context_class_name]
|
391
|
-
collection_name = config[:collection_name]
|
392
|
-
|
393
|
-
# Find all instances of context_class where this object appears
|
394
|
-
# This is simplified - in practice you'd need a more efficient approach
|
395
|
-
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
396
|
-
|
397
|
-
dbclient.scan_each(match: pattern) do |key|
|
398
|
-
score = dbclient.zscore(key, identifier)
|
399
|
-
if score
|
400
|
-
context_id = key.split(':')[1]
|
401
|
-
memberships << {
|
402
|
-
context_class: context_class_name,
|
403
|
-
context_id: context_id,
|
404
|
-
collection_name: collection_name,
|
405
|
-
score: score,
|
406
|
-
decoded_score: decode_score(score)
|
407
|
-
}
|
408
|
-
end
|
409
|
-
end
|
410
|
-
end
|
411
|
-
|
412
|
-
memberships
|
413
|
-
end
|
414
|
-
end
|
415
|
-
end
|
416
|
-
end
|
417
|
-
end
|
418
|
-
end
|