familia 2.0.0.pre15 → 2.0.0.pre16
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/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +64 -4
- data/CLAUDE.md +1 -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 +41 -41
- 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 +623 -19
- 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 +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -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/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- 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 +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- 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 +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- 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 +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -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 +3 -5
- data/lib/familia/features.rb +4 -13
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- 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 +1 -1
- 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 +7 -7
- 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 +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- 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 +1 -1
- 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/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/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 +433 -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 +1 -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 +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -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/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- 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
- metadata +75 -43
- 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/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
@@ -5,7 +5,7 @@ module Familia
|
|
5
5
|
module Relationships
|
6
6
|
# Score encoding using bit flags for permissions
|
7
7
|
#
|
8
|
-
# Encodes permissions as bit flags in the decimal portion of Redis sorted set scores:
|
8
|
+
# Encodes permissions as bit flags in the decimal portion of Valkey/Redis sorted set scores:
|
9
9
|
# - Integer part: Unix timestamp for time-based ordering
|
10
10
|
# - Decimal part: 8-bit permission flags (0-255)
|
11
11
|
#
|
@@ -48,7 +48,7 @@ module Familia
|
|
48
48
|
viewer: PERMISSION_FLAGS[:read],
|
49
49
|
editor: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit],
|
50
50
|
moderator: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit] | PERMISSION_FLAGS[:delete],
|
51
|
-
admin: 0b11111111 # All permissions
|
51
|
+
admin: 0b11111111, # All permissions
|
52
52
|
}.freeze
|
53
53
|
|
54
54
|
# Categorical masks for efficient broad queries
|
@@ -57,7 +57,7 @@ module Familia
|
|
57
57
|
content_editor: 0b00001110, # Can modify content (append|write|edit)
|
58
58
|
administrator: 0b11110000, # Has any admin powers
|
59
59
|
privileged: 0b11111110, # Has beyond read-only
|
60
|
-
owner: 0b11111111
|
60
|
+
owner: 0b11111111, # All permissions
|
61
61
|
}.freeze
|
62
62
|
|
63
63
|
class << self
|
@@ -74,7 +74,7 @@ module Familia
|
|
74
74
|
#
|
75
75
|
# @param timestamp [Time, Integer] The timestamp to encode
|
76
76
|
# @param permission [Symbol, Integer, Array] Permission(s) to encode
|
77
|
-
# @return [Float] Encoded score suitable for Redis sorted sets
|
77
|
+
# @return [Float] Encoded score suitable for Valkey/Redis sorted sets
|
78
78
|
def permission_encode(timestamp, permission)
|
79
79
|
encode_score(timestamp, permission)
|
80
80
|
end
|
@@ -88,26 +88,26 @@ module Familia
|
|
88
88
|
{
|
89
89
|
timestamp: decoded[:timestamp],
|
90
90
|
permissions: decoded[:permissions],
|
91
|
-
permission_list: decoded[:permission_list]
|
91
|
+
permission_list: decoded[:permission_list],
|
92
92
|
}
|
93
93
|
end
|
94
94
|
|
95
|
-
# Encode a timestamp and permissions into a Redis score
|
95
|
+
# Encode a timestamp and permissions into a Valkey/Redis score
|
96
96
|
#
|
97
97
|
# @param timestamp [Time, Integer] The timestamp to encode
|
98
98
|
# @param permissions [Integer, Symbol, Array] Permissions to encode
|
99
|
-
# @return [Float] Encoded score suitable for Redis sorted sets
|
99
|
+
# @return [Float] Encoded score suitable for Valkey/Redis sorted sets
|
100
100
|
#
|
101
101
|
# @example Basic encoding with bit flag
|
102
|
-
# encode_score(
|
102
|
+
# encode_score(Familia.now, 5) # read(1) + write(4) = 5
|
103
103
|
# #=> 1704067200.005
|
104
104
|
#
|
105
105
|
# @example Permission symbol encoding
|
106
|
-
# encode_score(
|
106
|
+
# encode_score(Familia.now, :read)
|
107
107
|
# #=> 1704067200.001
|
108
108
|
#
|
109
109
|
# @example Multiple permissions
|
110
|
-
# encode_score(
|
110
|
+
# encode_score(Familia.now, [:read, :write, :delete])
|
111
111
|
# #=> 1704067200.037
|
112
112
|
def encode_score(timestamp, permissions = 0)
|
113
113
|
time_part = timestamp.respond_to?(:to_i) ? timestamp.to_i : timestamp
|
@@ -127,7 +127,7 @@ module Familia
|
|
127
127
|
time_part + (permission_bits / METADATA_PRECISION)
|
128
128
|
end
|
129
129
|
|
130
|
-
# Decode a Redis score back into timestamp and permissions
|
130
|
+
# Decode a Valkey/Redis score back into timestamp and permissions
|
131
131
|
#
|
132
132
|
# @param score [Float] The encoded score
|
133
133
|
# @return [Hash] Hash with :timestamp, :permissions, and :permission_list keys
|
@@ -144,7 +144,7 @@ module Familia
|
|
144
144
|
{
|
145
145
|
timestamp: time_part,
|
146
146
|
permissions: permission_bits,
|
147
|
-
permission_list: decode_permission_flags(permission_bits)
|
147
|
+
permission_list: decode_permission_flags(permission_bits),
|
148
148
|
}
|
149
149
|
end
|
150
150
|
|
@@ -211,7 +211,7 @@ module Familia
|
|
211
211
|
#
|
212
212
|
# @param min_permissions [Array<Symbol>, nil] Minimum required permissions
|
213
213
|
# @param max_permissions [Array<Symbol>, nil] Maximum allowed permissions
|
214
|
-
# @return [Array<Float>] Min and max scores for Redis range queries
|
214
|
+
# @return [Array<Float>] Min and max scores for Valkey/Redis range queries
|
215
215
|
#
|
216
216
|
# @example
|
217
217
|
# permission_range([:read], [:read, :write])
|
@@ -231,20 +231,20 @@ module Familia
|
|
231
231
|
|
232
232
|
# Get current timestamp as score (no permissions)
|
233
233
|
#
|
234
|
-
# @return [Float] Current time as Redis score
|
234
|
+
# @return [Float] Current time as Valkey/Redis score
|
235
235
|
def current_score
|
236
|
-
encode_score(
|
236
|
+
encode_score(Familia.now, 0)
|
237
237
|
end
|
238
238
|
|
239
|
-
# Create score range for
|
239
|
+
# Create score range for db operations based on time bounds
|
240
240
|
#
|
241
241
|
# @param start_time [Time, nil] Start time (nil for -inf)
|
242
242
|
# @param end_time [Time, nil] End time (nil for +inf)
|
243
243
|
# @param min_permissions [Array<Symbol>, nil] Minimum required permissions
|
244
|
-
# @return [Array] Array suitable for Redis ZRANGEBYSCORE operations
|
244
|
+
# @return [Array] Array suitable for Valkey/Redis ZRANGEBYSCORE operations
|
245
245
|
#
|
246
246
|
# @example Time range
|
247
|
-
# score_range(1.hour.ago,
|
247
|
+
# score_range(1.hour.ago, Familia.now)
|
248
248
|
# #=> [1704063600.0, 1704067200.255]
|
249
249
|
#
|
250
250
|
# @example Permission filter
|
@@ -367,13 +367,13 @@ module Familia
|
|
367
367
|
# @param category [Symbol] Category to create range for
|
368
368
|
# @param start_time [Time, nil] Optional start time filter
|
369
369
|
# @param end_time [Time, nil] Optional end time filter
|
370
|
-
# @return [Array<String>] Min and max range strings for Redis queries
|
370
|
+
# @return [Array<String>] Min and max range strings for Valkey/Redis queries
|
371
371
|
def category_score_range(category, start_time = nil, end_time = nil)
|
372
372
|
PERMISSION_CATEGORIES[category] || 0
|
373
373
|
|
374
374
|
# Any permission matching the category mask
|
375
375
|
min_score = start_time ? start_time.to_i : 0
|
376
|
-
max_score = end_time ? end_time.to_i :
|
376
|
+
max_score = end_time ? end_time.to_i : Familia.now.to_i
|
377
377
|
|
378
378
|
# Return range that includes any matching permissions
|
379
379
|
["#{min_score}.000", "#{max_score}.999"]
|
@@ -2,80 +2,61 @@
|
|
2
2
|
|
3
3
|
require 'securerandom'
|
4
4
|
require_relative 'relationships/score_encoding'
|
5
|
-
require_relative 'relationships/
|
6
|
-
require_relative 'relationships/tracking'
|
5
|
+
require_relative 'relationships/participation'
|
7
6
|
require_relative 'relationships/indexing'
|
8
|
-
require_relative 'relationships/membership'
|
9
|
-
require_relative 'relationships/cascading'
|
10
|
-
require_relative 'relationships/querying'
|
11
|
-
require_relative 'relationships/permission_management'
|
12
7
|
|
13
8
|
module Familia
|
14
9
|
module Features
|
15
10
|
# Unified Relationships feature for Familia v2
|
16
11
|
#
|
17
12
|
# This feature merges the functionality of relatable_objects and relationships
|
18
|
-
# into a single, Redis-native implementation that embraces the "where does this appear?"
|
13
|
+
# into a single, Valkey/Redis-native implementation that embraces the "where does this appear?"
|
19
14
|
# philosophy rather than "who owns this?".
|
20
15
|
#
|
21
|
-
# Key improvements in v2:
|
22
|
-
# - Multi-presence: Objects can exist in multiple collections simultaneously
|
23
|
-
# - Score encoding: Metadata embedded in Redis scores for efficiency
|
24
|
-
# - Collision-free: Method names include collection names to prevent conflicts
|
25
|
-
# - Redis-native: All operations use Redis commands, no Ruby iteration
|
26
|
-
# - Atomic operations: Multi-collection updates happen atomically
|
27
|
-
#
|
28
|
-
# Breaking changes from v1:
|
29
|
-
# - Single feature: Use `feature :relationships` instead of separate features
|
30
|
-
# - Simplified identifier: Use `identifier :field` instead of `identifier_field :field`
|
31
|
-
# - No ownership concept: Remove `owned_by`, use multi-presence instead
|
32
|
-
# - Method naming: Generated methods include collection names for uniqueness
|
33
|
-
# - Score encoding: Scores can carry metadata like permissions
|
34
|
-
#
|
35
16
|
# @example Basic usage
|
36
17
|
# class Domain < Familia::Horreum
|
37
|
-
# feature :relationships
|
38
18
|
#
|
39
|
-
#
|
19
|
+
# identifier_field :domain_id
|
20
|
+
#
|
40
21
|
# field :domain_id
|
41
22
|
# field :display_name
|
42
23
|
# field :created_at
|
43
24
|
# field :permission_bits
|
44
25
|
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
26
|
+
# feature :relationships
|
27
|
+
#
|
28
|
+
# # Multi-presence participation with score encoding
|
29
|
+
# participates_in Customer, :domains,
|
30
|
+
# score: -> { permission_encode(created_at, permission_bits) }
|
31
|
+
# participates_in Team, :domains, score: :added_at
|
32
|
+
# participates_in Organization, :all_domains, score: :created_at
|
50
33
|
#
|
51
|
-
# # O(1) lookups with Redis hashes
|
52
|
-
# indexed_by :display_name, :domain_index,
|
53
|
-
# indexed_by :display_name, :global_domain_index, context: :global
|
34
|
+
# # O(1) lookups with Valkey/Redis hashes
|
35
|
+
# indexed_by :display_name, :domain_index, target: Customer
|
54
36
|
#
|
55
|
-
# #
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
37
|
+
# # Participation with bidirectional control (no method collisions)
|
38
|
+
# participates_in Customer, :domains
|
39
|
+
# participates_in Team, :domains, bidirectional: false
|
40
|
+
# participates_in Organization, :domains, type: :set
|
59
41
|
# end
|
60
42
|
#
|
61
43
|
# @example Generated methods (collision-free)
|
62
|
-
# #
|
44
|
+
# # Participation methods
|
63
45
|
# Customer.domains # => Familia::SortedSet
|
64
46
|
# Customer.add_domain(domain, score) # Add to customer's domains
|
65
47
|
# domain.in_customer_domains?(customer) # Check membership
|
66
48
|
#
|
67
49
|
# # Indexing methods
|
68
50
|
# Customer.find_by_display_name(name) # O(1) lookup
|
69
|
-
# Domain.find_by_display_name(name) # Global lookup
|
70
51
|
#
|
71
|
-
# #
|
52
|
+
# # Bidirectional methods (collision-free naming)
|
72
53
|
# domain.add_to_customer_domains(customer) # Specific collection
|
73
54
|
# domain.add_to_team_domains(team) # Different collection
|
74
55
|
# domain.in_customer_domains?(customer) # Check specific membership
|
75
56
|
#
|
76
57
|
# @example Score encoding for permissions
|
77
58
|
# # Encode permission in score
|
78
|
-
# score = domain.permission_encode(
|
59
|
+
# score = domain.permission_encode(Familia.now, :write)
|
79
60
|
# # => 1704067200.004 (timestamp + permission bits)
|
80
61
|
#
|
81
62
|
# # Decode permission from score
|
@@ -89,43 +70,32 @@ module Familia
|
|
89
70
|
# # Atomic updates across multiple collections
|
90
71
|
# domain.update_multiple_presence([
|
91
72
|
# { key: "customer:123:domains", score: current_score },
|
92
|
-
# { key: "team:456:domains", score: permission_encode(
|
73
|
+
# { key: "team:456:domains", score: permission_encode(Familia.now, :read) }
|
93
74
|
# ], :add, domain.identifier)
|
94
75
|
#
|
95
|
-
# #
|
76
|
+
# # UnsortedSet operations on collections
|
96
77
|
# accessible = Domain.union_collections([
|
97
78
|
# { owner: customer, collection: :domains },
|
98
79
|
# { owner: team, collection: :domains }
|
99
80
|
# ], min_permission: :read)
|
100
81
|
module Relationships
|
101
|
-
|
102
82
|
# Register the feature with Familia
|
103
83
|
Familia::Base.add_feature Relationships, :relationships
|
104
84
|
|
105
85
|
# Feature initialization
|
106
86
|
def self.included(base)
|
107
87
|
Familia.ld "[#{base}] Relationships included"
|
108
|
-
base.extend
|
109
|
-
base.include
|
88
|
+
base.extend ModelClassMethods
|
89
|
+
base.include ModelInstanceMethods
|
110
90
|
|
111
91
|
# Include all relationship submodules and their class methods
|
112
92
|
base.include ScoreEncoding
|
113
|
-
base.include RedisOperations
|
114
93
|
|
115
|
-
base.include
|
116
|
-
base.extend
|
94
|
+
base.include Participation
|
95
|
+
base.extend Participation::ModelClassMethods
|
117
96
|
|
118
97
|
base.include Indexing
|
119
|
-
base.extend Indexing::
|
120
|
-
|
121
|
-
base.include Membership
|
122
|
-
base.extend Membership::ClassMethods
|
123
|
-
|
124
|
-
base.include Cascading
|
125
|
-
base.extend Cascading::ClassMethods
|
126
|
-
|
127
|
-
base.include Querying
|
128
|
-
base.extend Querying::ClassMethods
|
98
|
+
base.extend Indexing::ModelClassMethods
|
129
99
|
end
|
130
100
|
|
131
101
|
# Error classes
|
@@ -134,7 +104,7 @@ module Familia
|
|
134
104
|
class InvalidScoreError < RelationshipError; end
|
135
105
|
class CascadeError < RelationshipError; end
|
136
106
|
|
137
|
-
module
|
107
|
+
module ModelClassMethods
|
138
108
|
# Define the identifier for this class (replaces identifier_field)
|
139
109
|
# This is a compatibility wrapper around the existing identifier_field method
|
140
110
|
#
|
@@ -149,21 +119,6 @@ module Familia
|
|
149
119
|
identifier_field
|
150
120
|
end
|
151
121
|
|
152
|
-
# Generate a secure temporary identifier
|
153
|
-
def generate_identifier
|
154
|
-
SecureRandom.hex(8)
|
155
|
-
end
|
156
|
-
|
157
|
-
# Get all relationship configurations for this class
|
158
|
-
def relationship_configs
|
159
|
-
configs = {}
|
160
|
-
|
161
|
-
configs[:tracking] = tracking_relationships if respond_to?(:tracking_relationships)
|
162
|
-
configs[:indexing] = indexing_relationships if respond_to?(:indexing_relationships)
|
163
|
-
configs[:membership] = membership_relationships if respond_to?(:membership_relationships)
|
164
|
-
|
165
|
-
configs
|
166
|
-
end
|
167
122
|
|
168
123
|
# Validate relationship configurations
|
169
124
|
def validate_relationships!
|
@@ -172,25 +127,14 @@ module Familia
|
|
172
127
|
# Check for method name collisions
|
173
128
|
method_names = []
|
174
129
|
|
175
|
-
if respond_to?(:
|
176
|
-
|
177
|
-
|
178
|
-
collection_name = config[:collection_name]
|
179
|
-
|
180
|
-
method_names << "in_#{context_name}_#{collection_name}?"
|
181
|
-
method_names << "add_to_#{context_name}_#{collection_name}"
|
182
|
-
method_names << "remove_from_#{context_name}_#{collection_name}"
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
if respond_to?(:membership_relationships)
|
187
|
-
membership_relationships.each do |config|
|
188
|
-
owner_name = config[:owner_class_name].downcase
|
130
|
+
if respond_to?(:participation_relationships)
|
131
|
+
participation_relationships.each do |config|
|
132
|
+
target_name = config[:target_class_name].downcase
|
189
133
|
collection_name = config[:collection_name]
|
190
134
|
|
191
|
-
method_names << "in_#{
|
192
|
-
method_names << "add_to_#{
|
193
|
-
method_names << "remove_from_#{
|
135
|
+
method_names << "in_#{target_name}_#{collection_name}?"
|
136
|
+
method_names << "add_to_#{target_name}_#{collection_name}"
|
137
|
+
method_names << "remove_from_#{target_name}_#{collection_name}"
|
194
138
|
end
|
195
139
|
end
|
196
140
|
|
@@ -209,20 +153,13 @@ module Familia
|
|
209
153
|
true
|
210
154
|
end
|
211
155
|
|
212
|
-
# Create a new instance with relationships initialized
|
213
|
-
def create_with_relationships(attributes = {})
|
214
|
-
instance = new(attributes)
|
215
|
-
instance.initialize_relationships
|
216
|
-
instance
|
217
|
-
end
|
218
|
-
|
219
156
|
# Class method wrapper for create_temp_key
|
220
157
|
def create_temp_key(base_name, ttl = 300)
|
221
|
-
timestamp =
|
158
|
+
timestamp = Familia.now.to_i
|
222
159
|
random_suffix = SecureRandom.hex(3)
|
223
160
|
temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
|
224
161
|
|
225
|
-
#
|
162
|
+
# UnsortedSet immediate expiry to ensure cleanup even if operation fails
|
226
163
|
if respond_to?(:dbclient)
|
227
164
|
dbclient.expire(temp_key, ttl)
|
228
165
|
else
|
@@ -236,116 +173,60 @@ module Familia
|
|
236
173
|
include ScoreEncoding
|
237
174
|
|
238
175
|
private
|
239
|
-
|
240
|
-
# Simple constantize method to convert string to constant
|
241
|
-
def constantize_class_name(class_name)
|
242
|
-
class_name.split('::').reduce(Object) { |mod, name| mod.const_get(name) }
|
243
|
-
rescue NameError
|
244
|
-
# If the class doesn't exist, return nil
|
245
|
-
nil
|
246
|
-
end
|
247
176
|
end
|
248
177
|
|
249
|
-
module
|
250
|
-
#
|
251
|
-
#
|
252
|
-
def identifier
|
253
|
-
id_field = self.class.identifier_field
|
254
|
-
send(id_field) if respond_to?(id_field)
|
255
|
-
end
|
256
|
-
|
257
|
-
# Set the identifier value for this instance
|
258
|
-
def identifier=(value)
|
259
|
-
id_field = self.class.identifier_field
|
260
|
-
send("#{id_field}=", value) if respond_to?("#{id_field}=")
|
261
|
-
end
|
262
|
-
|
263
|
-
# Initialize relationships (called after object creation)
|
264
|
-
def initialize_relationships
|
265
|
-
# This can be overridden by subclasses to set up initial relationships
|
266
|
-
end
|
178
|
+
module ModelInstanceMethods
|
179
|
+
# NOTE: identifier and identifier= methods are provided by Horreum base class
|
180
|
+
# No need to override them here - use the existing infrastructure
|
267
181
|
|
268
182
|
# Override save to update relationships automatically
|
269
183
|
def save(update_expiration: true)
|
270
184
|
result = super
|
271
185
|
|
272
|
-
if result
|
186
|
+
if result && respond_to?(:update_all_indexes)
|
273
187
|
# Automatically update all indexes when object is saved
|
274
|
-
|
275
|
-
update_all_indexes
|
276
|
-
end
|
277
|
-
|
278
|
-
# Auto-add to class-level tracking collections
|
279
|
-
if respond_to?(:add_to_class_tracking_collections)
|
280
|
-
add_to_class_tracking_collections
|
281
|
-
end
|
188
|
+
update_all_indexes
|
282
189
|
|
283
|
-
# NOTE: Relationship-specific
|
190
|
+
# NOTE: Relationship-specific participation updates are done explicitly
|
284
191
|
# since we need to know which specific collections this object should be in
|
285
192
|
end
|
286
193
|
|
287
194
|
result
|
288
195
|
end
|
289
196
|
|
290
|
-
# Override destroy to handle cascade operations
|
291
|
-
def destroy!
|
292
|
-
# Execute cascade operations before destroying the object
|
293
|
-
execute_cascade_operations if respond_to?(:execute_cascade_operations)
|
294
|
-
|
295
|
-
super
|
296
|
-
end
|
297
|
-
|
298
197
|
# Get comprehensive relationship status for this object
|
299
198
|
def relationship_status
|
300
199
|
status = {
|
301
200
|
identifier: identifier,
|
302
|
-
|
303
|
-
|
304
|
-
index_memberships: []
|
201
|
+
current_participations: [],
|
202
|
+
index_memberships: [],
|
305
203
|
}
|
306
204
|
|
307
|
-
# Get
|
308
|
-
if respond_to?(:
|
309
|
-
status[:tracking_memberships] = tracking_collections_membership
|
310
|
-
end
|
311
|
-
|
312
|
-
# Get membership collections
|
313
|
-
status[:membership_collections] = membership_collections if respond_to?(:membership_collections)
|
205
|
+
# Get participation memberships
|
206
|
+
status[:current_participations] = current_participations if respond_to?(:current_participations)
|
314
207
|
|
315
208
|
# Get index memberships
|
316
|
-
status[:index_memberships] =
|
209
|
+
status[:index_memberships] = current_indexings if respond_to?(:current_indexings)
|
317
210
|
|
318
211
|
status
|
319
212
|
end
|
320
213
|
|
321
214
|
# Comprehensive cleanup - remove from all relationships
|
215
|
+
#
|
216
|
+
# @deprecated This method is poorly implemented and will be removed in v3.0.
|
217
|
+
# The participation collection removal logic was repetitive and difficult to debug.
|
218
|
+
# A cleaner implementation will be provided in a future version.
|
219
|
+
# See pull #115 for details.
|
220
|
+
#
|
221
|
+
# @note Currently only removes from indexes, not participation collections
|
322
222
|
def cleanup_all_relationships!
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
# Remove from membership collections
|
327
|
-
remove_from_all_memberships if respond_to?(:remove_from_all_memberships)
|
223
|
+
warn '[DEPRECATED] cleanup_all_relationships! will be removed in v3.0. See pull #115.'
|
224
|
+
warn 'Not currently removing from participation collections. Only indexes will be cleaned.'
|
328
225
|
|
329
226
|
# Remove from indexes
|
330
227
|
remove_from_all_indexes if respond_to?(:remove_from_all_indexes)
|
331
228
|
end
|
332
229
|
|
333
|
-
# Dry run for relationship cleanup (preview what would be affected)
|
334
|
-
def cleanup_preview
|
335
|
-
preview = {
|
336
|
-
tracking_collections: [],
|
337
|
-
membership_collections: [],
|
338
|
-
index_entries: []
|
339
|
-
}
|
340
|
-
|
341
|
-
if respond_to?(:cascade_dry_run)
|
342
|
-
cascade_preview = cascade_dry_run
|
343
|
-
preview.merge!(cascade_preview)
|
344
|
-
end
|
345
|
-
|
346
|
-
preview
|
347
|
-
end
|
348
|
-
|
349
230
|
# Validate that this object's relationships are consistent
|
350
231
|
def validate_relationships!
|
351
232
|
errors = []
|
@@ -353,11 +234,11 @@ module Familia
|
|
353
234
|
# Validate identifier exists
|
354
235
|
errors << 'Object identifier is nil' unless identifier
|
355
236
|
|
356
|
-
# Validate
|
357
|
-
if respond_to?(:
|
358
|
-
|
237
|
+
# Validate participation memberships
|
238
|
+
if respond_to?(:current_participations)
|
239
|
+
current_participations.each do |membership|
|
359
240
|
score = membership[:score]
|
360
|
-
errors << "Invalid score in
|
241
|
+
errors << "Invalid score in participation membership: #{membership}" if score && !score.is_a?(Numeric)
|
361
242
|
end
|
362
243
|
end
|
363
244
|
|
@@ -366,107 +247,25 @@ module Familia
|
|
366
247
|
true
|
367
248
|
end
|
368
249
|
|
369
|
-
#
|
370
|
-
def
|
371
|
-
# Clear any cached relationship data
|
372
|
-
@relationship_status = nil
|
373
|
-
@tracking_memberships = nil
|
374
|
-
@membership_collections = nil
|
375
|
-
@index_memberships = nil
|
376
|
-
|
377
|
-
# Reload fresh data
|
378
|
-
relationship_status
|
379
|
-
end
|
380
|
-
|
381
|
-
# Create a snapshot of current relationship state (for debugging)
|
382
|
-
def relationship_snapshot
|
383
|
-
{
|
384
|
-
timestamp: Time.now,
|
385
|
-
identifier: identifier,
|
386
|
-
class: self.class.name,
|
387
|
-
status: relationship_status,
|
388
|
-
redis_keys: find_related_redis_keys
|
389
|
-
}
|
390
|
-
end
|
391
|
-
|
392
|
-
# Direct Redis access for instance methods
|
393
|
-
def redis
|
250
|
+
# Direct Valkey/Redis access for instance methods
|
251
|
+
def dbclient
|
394
252
|
self.class.dbclient
|
395
253
|
end
|
396
254
|
|
397
255
|
# Instance method wrapper for create_temp_key
|
398
256
|
def create_temp_key(base_name, ttl = 300)
|
399
|
-
timestamp =
|
257
|
+
timestamp = Familia.now.to_i
|
400
258
|
random_suffix = SecureRandom.hex(3)
|
401
259
|
temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
|
402
260
|
|
403
|
-
#
|
404
|
-
|
261
|
+
# UnsortedSet immediate expiry to ensure cleanup even if operation fails
|
262
|
+
dbclient.expire(temp_key, ttl)
|
405
263
|
|
406
264
|
temp_key
|
407
265
|
end
|
408
266
|
|
409
|
-
# Instance method wrapper for cleanup_temp_keys
|
410
|
-
def cleanup_temp_keys(pattern = 'temp:*', batch_size = 100)
|
411
|
-
cursor = 0
|
412
|
-
|
413
|
-
loop do
|
414
|
-
cursor, keys = redis.scan(cursor, match: pattern, count: batch_size)
|
415
|
-
|
416
|
-
if keys.any?
|
417
|
-
# Check TTL and remove keys that should have expired
|
418
|
-
keys.each_slice(batch_size) do |key_batch|
|
419
|
-
redis.pipelined do |pipeline|
|
420
|
-
key_batch.each do |key|
|
421
|
-
ttl = redis.ttl(key)
|
422
|
-
pipeline.del(key) if ttl == -1 # Key exists but has no TTL
|
423
|
-
end
|
424
|
-
end
|
425
|
-
end
|
426
|
-
end
|
427
|
-
|
428
|
-
break if cursor.zero?
|
429
|
-
end
|
430
|
-
end
|
431
|
-
|
432
267
|
private
|
433
|
-
|
434
|
-
# Find all Redis keys related to this object
|
435
|
-
def find_related_redis_keys
|
436
|
-
related_keys = []
|
437
|
-
id = identifier
|
438
|
-
return related_keys unless id
|
439
|
-
|
440
|
-
# Scan for keys that might contain this object
|
441
|
-
patterns = [
|
442
|
-
'*:*:*', # General pattern for relationship keys
|
443
|
-
"*#{id}*" # Keys containing the identifier
|
444
|
-
]
|
445
|
-
|
446
|
-
patterns.each do |pattern|
|
447
|
-
redis.scan_each(match: pattern, count: 100) do |key|
|
448
|
-
# Check if this key actually contains our object
|
449
|
-
key_type = redis.type(key)
|
450
|
-
|
451
|
-
case key_type
|
452
|
-
when 'zset'
|
453
|
-
related_keys << key if redis.zscore(key, id)
|
454
|
-
when 'set'
|
455
|
-
related_keys << key if redis.sismember(key, id)
|
456
|
-
when 'list'
|
457
|
-
related_keys << key if redis.lpos(key, id)
|
458
|
-
when 'hash'
|
459
|
-
# For hash keys, check if any field values match our identifier
|
460
|
-
hash_values = redis.hvals(key)
|
461
|
-
related_keys << key if hash_values.include?(id.to_s)
|
462
|
-
end
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
466
|
-
related_keys.uniq
|
467
|
-
end
|
468
268
|
end
|
469
|
-
|
470
269
|
end
|
471
270
|
end
|
472
271
|
end
|