familia 2.0.0.pre14 → 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 +66 -6
- 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 +4 -4
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre13.md +1 -1
- 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/{autoloader.rb → features/autoloader.rb} +49 -23
- 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 +68 -66
- data/lib/familia/features/expiration/extensions.rb +61 -0
- data/lib/familia/features/expiration.rb +35 -87
- data/lib/familia/features/external_identifier.rb +11 -12
- data/lib/familia/features/object_identifier.rb +58 -20
- data/lib/familia/features/quantization.rb +17 -22
- 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 +69 -271
- data/lib/familia/features/safe_dump.rb +127 -132
- 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 +5 -5
- data/lib/familia/features.rb +21 -21
- 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 -15
- 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 +129 -11
- 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 +77 -45
- 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 -228
- 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/autoloadable.rb +0 -113
- 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/autoloadable/autoloadable_try.rb +0 -61
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
- 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
|
-
# tracked_in Customer, :domains,
|
47
|
-
# score: -> { permission_encode(created_at, permission_bits) }
|
48
|
-
# tracked_in Team, :domains, score: :added_at
|
49
|
-
# tracked_in Organization, :all_domains, score: :created_at
|
26
|
+
# feature :relationships
|
50
27
|
#
|
51
|
-
# #
|
52
|
-
#
|
53
|
-
#
|
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
|
54
33
|
#
|
55
|
-
# #
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
34
|
+
# # O(1) lookups with Valkey/Redis hashes
|
35
|
+
# indexed_by :display_name, :domain_index, target: Customer
|
36
|
+
#
|
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,42 +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
|
82
|
+
# Register the feature with Familia
|
83
|
+
Familia::Base.add_feature Relationships, :relationships
|
84
|
+
|
101
85
|
# Feature initialization
|
102
86
|
def self.included(base)
|
103
|
-
|
104
|
-
base.extend
|
105
|
-
base.include
|
87
|
+
Familia.ld "[#{base}] Relationships included"
|
88
|
+
base.extend ModelClassMethods
|
89
|
+
base.include ModelInstanceMethods
|
106
90
|
|
107
91
|
# Include all relationship submodules and their class methods
|
108
92
|
base.include ScoreEncoding
|
109
|
-
base.include RedisOperations
|
110
93
|
|
111
|
-
|
112
|
-
base.
|
113
|
-
puts '[DEBUG] Extending with Tracking::ClassMethods'
|
114
|
-
base.extend Tracking::ClassMethods
|
115
|
-
puts "[DEBUG] Base now responds to tracked_in: #{base.respond_to?(:tracked_in)}"
|
94
|
+
base.include Participation
|
95
|
+
base.extend Participation::ModelClassMethods
|
116
96
|
|
117
97
|
base.include Indexing
|
118
|
-
base.extend Indexing::
|
119
|
-
|
120
|
-
base.include Membership
|
121
|
-
base.extend Membership::ClassMethods
|
122
|
-
|
123
|
-
base.include Cascading
|
124
|
-
base.extend Cascading::ClassMethods
|
125
|
-
|
126
|
-
base.include Querying
|
127
|
-
base.extend Querying::ClassMethods
|
98
|
+
base.extend Indexing::ModelClassMethods
|
128
99
|
end
|
129
100
|
|
130
101
|
# Error classes
|
@@ -133,7 +104,7 @@ module Familia
|
|
133
104
|
class InvalidScoreError < RelationshipError; end
|
134
105
|
class CascadeError < RelationshipError; end
|
135
106
|
|
136
|
-
module
|
107
|
+
module ModelClassMethods
|
137
108
|
# Define the identifier for this class (replaces identifier_field)
|
138
109
|
# This is a compatibility wrapper around the existing identifier_field method
|
139
110
|
#
|
@@ -148,21 +119,6 @@ module Familia
|
|
148
119
|
identifier_field
|
149
120
|
end
|
150
121
|
|
151
|
-
# Generate a secure temporary identifier
|
152
|
-
def generate_identifier
|
153
|
-
SecureRandom.hex(8)
|
154
|
-
end
|
155
|
-
|
156
|
-
# Get all relationship configurations for this class
|
157
|
-
def relationship_configs
|
158
|
-
configs = {}
|
159
|
-
|
160
|
-
configs[:tracking] = tracking_relationships if respond_to?(:tracking_relationships)
|
161
|
-
configs[:indexing] = indexing_relationships if respond_to?(:indexing_relationships)
|
162
|
-
configs[:membership] = membership_relationships if respond_to?(:membership_relationships)
|
163
|
-
|
164
|
-
configs
|
165
|
-
end
|
166
122
|
|
167
123
|
# Validate relationship configurations
|
168
124
|
def validate_relationships!
|
@@ -171,25 +127,14 @@ module Familia
|
|
171
127
|
# Check for method name collisions
|
172
128
|
method_names = []
|
173
129
|
|
174
|
-
if respond_to?(:
|
175
|
-
|
176
|
-
|
130
|
+
if respond_to?(:participation_relationships)
|
131
|
+
participation_relationships.each do |config|
|
132
|
+
target_name = config[:target_class_name].downcase
|
177
133
|
collection_name = config[:collection_name]
|
178
134
|
|
179
|
-
method_names << "in_#{
|
180
|
-
method_names << "add_to_#{
|
181
|
-
method_names << "remove_from_#{
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
if respond_to?(:membership_relationships)
|
186
|
-
membership_relationships.each do |config|
|
187
|
-
owner_name = config[:owner_class_name].downcase
|
188
|
-
collection_name = config[:collection_name]
|
189
|
-
|
190
|
-
method_names << "in_#{owner_name}_#{collection_name}?"
|
191
|
-
method_names << "add_to_#{owner_name}_#{collection_name}"
|
192
|
-
method_names << "remove_from_#{owner_name}_#{collection_name}"
|
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}"
|
193
138
|
end
|
194
139
|
end
|
195
140
|
|
@@ -208,20 +153,13 @@ module Familia
|
|
208
153
|
true
|
209
154
|
end
|
210
155
|
|
211
|
-
# Create a new instance with relationships initialized
|
212
|
-
def create_with_relationships(attributes = {})
|
213
|
-
instance = new(attributes)
|
214
|
-
instance.initialize_relationships
|
215
|
-
instance
|
216
|
-
end
|
217
|
-
|
218
156
|
# Class method wrapper for create_temp_key
|
219
157
|
def create_temp_key(base_name, ttl = 300)
|
220
|
-
timestamp =
|
158
|
+
timestamp = Familia.now.to_i
|
221
159
|
random_suffix = SecureRandom.hex(3)
|
222
160
|
temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
|
223
161
|
|
224
|
-
#
|
162
|
+
# UnsortedSet immediate expiry to ensure cleanup even if operation fails
|
225
163
|
if respond_to?(:dbclient)
|
226
164
|
dbclient.expire(temp_key, ttl)
|
227
165
|
else
|
@@ -235,116 +173,60 @@ module Familia
|
|
235
173
|
include ScoreEncoding
|
236
174
|
|
237
175
|
private
|
238
|
-
|
239
|
-
# Simple constantize method to convert string to constant
|
240
|
-
def constantize_class_name(class_name)
|
241
|
-
class_name.split('::').reduce(Object) { |mod, name| mod.const_get(name) }
|
242
|
-
rescue NameError
|
243
|
-
# If the class doesn't exist, return nil
|
244
|
-
nil
|
245
|
-
end
|
246
176
|
end
|
247
177
|
|
248
|
-
module
|
249
|
-
#
|
250
|
-
#
|
251
|
-
def identifier
|
252
|
-
id_field = self.class.identifier_field
|
253
|
-
send(id_field) if respond_to?(id_field)
|
254
|
-
end
|
255
|
-
|
256
|
-
# Set the identifier value for this instance
|
257
|
-
def identifier=(value)
|
258
|
-
id_field = self.class.identifier_field
|
259
|
-
send("#{id_field}=", value) if respond_to?("#{id_field}=")
|
260
|
-
end
|
261
|
-
|
262
|
-
# Initialize relationships (called after object creation)
|
263
|
-
def initialize_relationships
|
264
|
-
# This can be overridden by subclasses to set up initial relationships
|
265
|
-
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
|
266
181
|
|
267
182
|
# Override save to update relationships automatically
|
268
183
|
def save(update_expiration: true)
|
269
184
|
result = super
|
270
185
|
|
271
|
-
if result
|
186
|
+
if result && respond_to?(:update_all_indexes)
|
272
187
|
# Automatically update all indexes when object is saved
|
273
|
-
|
274
|
-
update_all_indexes
|
275
|
-
end
|
276
|
-
|
277
|
-
# Auto-add to class-level tracking collections
|
278
|
-
if respond_to?(:add_to_class_tracking_collections)
|
279
|
-
add_to_class_tracking_collections
|
280
|
-
end
|
188
|
+
update_all_indexes
|
281
189
|
|
282
|
-
# NOTE: Relationship-specific
|
190
|
+
# NOTE: Relationship-specific participation updates are done explicitly
|
283
191
|
# since we need to know which specific collections this object should be in
|
284
192
|
end
|
285
193
|
|
286
194
|
result
|
287
195
|
end
|
288
196
|
|
289
|
-
# Override destroy to handle cascade operations
|
290
|
-
def destroy!
|
291
|
-
# Execute cascade operations before destroying the object
|
292
|
-
execute_cascade_operations if respond_to?(:execute_cascade_operations)
|
293
|
-
|
294
|
-
super
|
295
|
-
end
|
296
|
-
|
297
197
|
# Get comprehensive relationship status for this object
|
298
198
|
def relationship_status
|
299
199
|
status = {
|
300
200
|
identifier: identifier,
|
301
|
-
|
302
|
-
|
303
|
-
index_memberships: []
|
201
|
+
current_participations: [],
|
202
|
+
index_memberships: [],
|
304
203
|
}
|
305
204
|
|
306
|
-
# Get
|
307
|
-
if respond_to?(:
|
308
|
-
status[:tracking_memberships] = tracking_collections_membership
|
309
|
-
end
|
310
|
-
|
311
|
-
# Get membership collections
|
312
|
-
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)
|
313
207
|
|
314
208
|
# Get index memberships
|
315
|
-
status[:index_memberships] =
|
209
|
+
status[:index_memberships] = current_indexings if respond_to?(:current_indexings)
|
316
210
|
|
317
211
|
status
|
318
212
|
end
|
319
213
|
|
320
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
|
321
222
|
def cleanup_all_relationships!
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
# Remove from membership collections
|
326
|
-
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.'
|
327
225
|
|
328
226
|
# Remove from indexes
|
329
227
|
remove_from_all_indexes if respond_to?(:remove_from_all_indexes)
|
330
228
|
end
|
331
229
|
|
332
|
-
# Dry run for relationship cleanup (preview what would be affected)
|
333
|
-
def cleanup_preview
|
334
|
-
preview = {
|
335
|
-
tracking_collections: [],
|
336
|
-
membership_collections: [],
|
337
|
-
index_entries: []
|
338
|
-
}
|
339
|
-
|
340
|
-
if respond_to?(:cascade_dry_run)
|
341
|
-
cascade_preview = cascade_dry_run
|
342
|
-
preview.merge!(cascade_preview)
|
343
|
-
end
|
344
|
-
|
345
|
-
preview
|
346
|
-
end
|
347
|
-
|
348
230
|
# Validate that this object's relationships are consistent
|
349
231
|
def validate_relationships!
|
350
232
|
errors = []
|
@@ -352,11 +234,11 @@ module Familia
|
|
352
234
|
# Validate identifier exists
|
353
235
|
errors << 'Object identifier is nil' unless identifier
|
354
236
|
|
355
|
-
# Validate
|
356
|
-
if respond_to?(:
|
357
|
-
|
237
|
+
# Validate participation memberships
|
238
|
+
if respond_to?(:current_participations)
|
239
|
+
current_participations.each do |membership|
|
358
240
|
score = membership[:score]
|
359
|
-
errors << "Invalid score in
|
241
|
+
errors << "Invalid score in participation membership: #{membership}" if score && !score.is_a?(Numeric)
|
360
242
|
end
|
361
243
|
end
|
362
244
|
|
@@ -365,109 +247,25 @@ module Familia
|
|
365
247
|
true
|
366
248
|
end
|
367
249
|
|
368
|
-
#
|
369
|
-
def
|
370
|
-
# Clear any cached relationship data
|
371
|
-
@relationship_status = nil
|
372
|
-
@tracking_memberships = nil
|
373
|
-
@membership_collections = nil
|
374
|
-
@index_memberships = nil
|
375
|
-
|
376
|
-
# Reload fresh data
|
377
|
-
relationship_status
|
378
|
-
end
|
379
|
-
|
380
|
-
# Create a snapshot of current relationship state (for debugging)
|
381
|
-
def relationship_snapshot
|
382
|
-
{
|
383
|
-
timestamp: Time.now,
|
384
|
-
identifier: identifier,
|
385
|
-
class: self.class.name,
|
386
|
-
status: relationship_status,
|
387
|
-
redis_keys: find_related_redis_keys
|
388
|
-
}
|
389
|
-
end
|
390
|
-
|
391
|
-
# Direct Redis access for instance methods
|
392
|
-
def redis
|
250
|
+
# Direct Valkey/Redis access for instance methods
|
251
|
+
def dbclient
|
393
252
|
self.class.dbclient
|
394
253
|
end
|
395
254
|
|
396
255
|
# Instance method wrapper for create_temp_key
|
397
256
|
def create_temp_key(base_name, ttl = 300)
|
398
|
-
timestamp =
|
257
|
+
timestamp = Familia.now.to_i
|
399
258
|
random_suffix = SecureRandom.hex(3)
|
400
259
|
temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
|
401
260
|
|
402
|
-
#
|
403
|
-
|
261
|
+
# UnsortedSet immediate expiry to ensure cleanup even if operation fails
|
262
|
+
dbclient.expire(temp_key, ttl)
|
404
263
|
|
405
264
|
temp_key
|
406
265
|
end
|
407
266
|
|
408
|
-
# Instance method wrapper for cleanup_temp_keys
|
409
|
-
def cleanup_temp_keys(pattern = 'temp:*', batch_size = 100)
|
410
|
-
cursor = 0
|
411
|
-
|
412
|
-
loop do
|
413
|
-
cursor, keys = redis.scan(cursor, match: pattern, count: batch_size)
|
414
|
-
|
415
|
-
if keys.any?
|
416
|
-
# Check TTL and remove keys that should have expired
|
417
|
-
keys.each_slice(batch_size) do |key_batch|
|
418
|
-
redis.pipelined do |pipeline|
|
419
|
-
key_batch.each do |key|
|
420
|
-
ttl = redis.ttl(key)
|
421
|
-
pipeline.del(key) if ttl == -1 # Key exists but has no TTL
|
422
|
-
end
|
423
|
-
end
|
424
|
-
end
|
425
|
-
end
|
426
|
-
|
427
|
-
break if cursor.zero?
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
267
|
private
|
432
|
-
|
433
|
-
# Find all Redis keys related to this object
|
434
|
-
def find_related_redis_keys
|
435
|
-
related_keys = []
|
436
|
-
id = identifier
|
437
|
-
return related_keys unless id
|
438
|
-
|
439
|
-
# Scan for keys that might contain this object
|
440
|
-
patterns = [
|
441
|
-
'*:*:*', # General pattern for relationship keys
|
442
|
-
"*#{id}*" # Keys containing the identifier
|
443
|
-
]
|
444
|
-
|
445
|
-
patterns.each do |pattern|
|
446
|
-
redis.scan_each(match: pattern, count: 100) do |key|
|
447
|
-
# Check if this key actually contains our object
|
448
|
-
key_type = redis.type(key)
|
449
|
-
|
450
|
-
case key_type
|
451
|
-
when 'zset'
|
452
|
-
related_keys << key if redis.zscore(key, id)
|
453
|
-
when 'set'
|
454
|
-
related_keys << key if redis.sismember(key, id)
|
455
|
-
when 'list'
|
456
|
-
related_keys << key if redis.lpos(key, id)
|
457
|
-
when 'hash'
|
458
|
-
# For hash keys, check if any field values match our identifier
|
459
|
-
hash_values = redis.hvals(key)
|
460
|
-
related_keys << key if hash_values.include?(id.to_s)
|
461
|
-
end
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
related_keys.uniq
|
466
|
-
end
|
467
268
|
end
|
468
|
-
|
469
|
-
# Register the feature with Familia
|
470
|
-
Familia::Base.add_feature Relationships, :relationships
|
471
269
|
end
|
472
270
|
end
|
473
271
|
end
|