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
data/examples/permissions.rb
DELETED
@@ -1,240 +0,0 @@
|
|
1
|
-
# examples/bit_encoding_integration.rb
|
2
|
-
#
|
3
|
-
# Production Integration Example: Document Management System with Fine-Grained Permissions
|
4
|
-
#
|
5
|
-
# This example demonstrates how to use Familia's bit encoding permission system
|
6
|
-
# in a real-world document management scenario with sophisticated access control.
|
7
|
-
|
8
|
-
require_relative '../lib/familia'
|
9
|
-
require_relative '../lib/familia/features/relationships/score_encoding'
|
10
|
-
require_relative '../lib/familia/features/relationships/permission_management'
|
11
|
-
|
12
|
-
# Document Management System Classes
|
13
|
-
class User < Familia::Horreum
|
14
|
-
logical_database 14
|
15
|
-
|
16
|
-
identifier_field :user_id
|
17
|
-
field :user_id
|
18
|
-
field :email
|
19
|
-
field :name
|
20
|
-
field :role # admin, editor, viewer, guest
|
21
|
-
field :created_at
|
22
|
-
|
23
|
-
sorted_set :documents # Documents this user can access
|
24
|
-
sorted_set :recent_activity # Recent document access
|
25
|
-
end
|
26
|
-
|
27
|
-
class Document < Familia::Horreum
|
28
|
-
include Familia::Features::Relationships::PermissionManagement
|
29
|
-
|
30
|
-
logical_database 14
|
31
|
-
|
32
|
-
# Enable fine-grained permission tracking
|
33
|
-
permission_tracking :user_permissions
|
34
|
-
|
35
|
-
identifier_field :doc_id
|
36
|
-
field :doc_id
|
37
|
-
field :title
|
38
|
-
field :owner_id
|
39
|
-
field :content
|
40
|
-
field :created_at
|
41
|
-
field :updated_at
|
42
|
-
field :document_type # public, private, confidential
|
43
|
-
|
44
|
-
sorted_set :collaborators # Users with access to this document
|
45
|
-
list :audit_log # Track permission changes and access
|
46
|
-
|
47
|
-
# Add document to user's collection with specific permissions
|
48
|
-
def share_with_user(user, *permissions)
|
49
|
-
permissions = [:read] if permissions.empty?
|
50
|
-
|
51
|
-
# Create time-based score with permissions encoded
|
52
|
-
timestamp = updated_at || Time.now
|
53
|
-
score = Familia::Features::Relationships::ScoreEncoding.encode_score(timestamp, permissions)
|
54
|
-
|
55
|
-
# Add to user's document list
|
56
|
-
user.documents.add(score, doc_id)
|
57
|
-
|
58
|
-
# Add user to document's collaborator list
|
59
|
-
collaborators.add(score, user.user_id)
|
60
|
-
|
61
|
-
# Grant permissions via permission management
|
62
|
-
grant(user, *permissions)
|
63
|
-
|
64
|
-
# Log the permission grant
|
65
|
-
log_entry = "#{Time.now.iso8601}: Granted #{permissions.join(', ')} to #{user.email}"
|
66
|
-
audit_log.push(log_entry)
|
67
|
-
end
|
68
|
-
|
69
|
-
# Remove user access
|
70
|
-
def revoke_access(user)
|
71
|
-
user.documents.zrem(doc_id)
|
72
|
-
collaborators.zrem(user.user_id)
|
73
|
-
revoke(user, :read, :write, :edit, :delete, :configure, :transfer, :admin)
|
74
|
-
|
75
|
-
log_entry = "#{Time.now.iso8601}: Revoked all access from #{user.email}"
|
76
|
-
audit_log.push(log_entry)
|
77
|
-
end
|
78
|
-
|
79
|
-
# Get users with specific permission level or higher
|
80
|
-
def users_with_permission(*required_permissions)
|
81
|
-
all_permissions.select do |_user_id, user_perms|
|
82
|
-
required_permissions.all? { |perm| user_perms.include?(perm) }
|
83
|
-
end.keys
|
84
|
-
end
|
85
|
-
|
86
|
-
# Advanced: Get document access history for analytics
|
87
|
-
def access_analytics(days_back = 30)
|
88
|
-
start_time = Time.now - (days_back * 24 * 60 * 60)
|
89
|
-
end_time = Time.now
|
90
|
-
|
91
|
-
# Use score range to get recent access
|
92
|
-
range = Familia::Features::Relationships::ScoreEncoding.score_range(
|
93
|
-
start_time,
|
94
|
-
end_time,
|
95
|
-
min_permissions: [:read]
|
96
|
-
)
|
97
|
-
|
98
|
-
# Get collaborators active in time range
|
99
|
-
active_users = collaborators.rangebyscore(*range)
|
100
|
-
|
101
|
-
{
|
102
|
-
active_users: active_users,
|
103
|
-
total_collaborators: collaborators.size,
|
104
|
-
permission_breakdown: all_permissions,
|
105
|
-
audit_entries: audit_log.range(0, 50),
|
106
|
-
}
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
# Document Management Service - Business Logic Layer
|
111
|
-
class DocumentService
|
112
|
-
# Permission role definitions matching business needs
|
113
|
-
ROLE_PERMISSIONS = {
|
114
|
-
guest: [:read],
|
115
|
-
viewer: [:read],
|
116
|
-
commenter: %i[read append],
|
117
|
-
editor: %i[read write edit],
|
118
|
-
reviewer: %i[read write edit delete],
|
119
|
-
admin: %i[read write edit delete configure transfer admin],
|
120
|
-
}.freeze
|
121
|
-
|
122
|
-
def self.create_document(owner, title, content, doc_type = 'private')
|
123
|
-
doc = Document.new(
|
124
|
-
doc_id: "doc_#{Time.now.to_i}_#{rand(1000)}",
|
125
|
-
title: title,
|
126
|
-
content: content,
|
127
|
-
owner_id: owner.user_id,
|
128
|
-
document_type: doc_type,
|
129
|
-
created_at: Time.now,
|
130
|
-
updated_at: Time.now
|
131
|
-
)
|
132
|
-
|
133
|
-
# Owner gets full admin access
|
134
|
-
doc.share_with_user(owner, *ROLE_PERMISSIONS[:admin])
|
135
|
-
doc
|
136
|
-
end
|
137
|
-
|
138
|
-
def self.share_document(document, user, role)
|
139
|
-
permissions = ROLE_PERMISSIONS[role] || ROLE_PERMISSIONS[:viewer]
|
140
|
-
document.share_with_user(user, *permissions)
|
141
|
-
end
|
142
|
-
|
143
|
-
def self.can_user_perform?(user, document, action)
|
144
|
-
case action
|
145
|
-
when :view, :read
|
146
|
-
document.can?(user, :read)
|
147
|
-
when :comment, :append
|
148
|
-
document.can?(user, :read, :append)
|
149
|
-
when :edit, :modify
|
150
|
-
document.can?(user, :read, :write, :edit)
|
151
|
-
when :delete, :remove
|
152
|
-
document.can?(user, :delete)
|
153
|
-
when :share, :configure
|
154
|
-
document.can?(user, :configure)
|
155
|
-
when :transfer_ownership
|
156
|
-
document.can?(user, :admin)
|
157
|
-
else
|
158
|
-
false
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def self.bulk_permission_update(documents, users, role)
|
163
|
-
permissions = ROLE_PERMISSIONS[role]
|
164
|
-
|
165
|
-
documents.each do |doc|
|
166
|
-
users.each do |user|
|
167
|
-
doc.revoke_access(user) # Clear existing
|
168
|
-
doc.share_with_user(user, *permissions) if permissions
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
# Example Usage and Demonstration
|
175
|
-
if __FILE__ == $0
|
176
|
-
puts '🚀 Familia Bit Encoding Integration Example'
|
177
|
-
puts '=' * 50
|
178
|
-
|
179
|
-
# Create users
|
180
|
-
alice = User.new(user_id: 'alice', email: 'alice@company.com', name: 'Alice Smith', role: 'admin')
|
181
|
-
bob = User.new(user_id: 'bob', email: 'bob@company.com', name: 'Bob Jones', role: 'editor')
|
182
|
-
charlie = User.new(user_id: 'charlie', email: 'charlie@company.com', name: 'Charlie Brown', role: 'viewer')
|
183
|
-
|
184
|
-
# Create documents
|
185
|
-
doc1 = DocumentService.create_document(alice, 'Q4 Financial Report', 'Confidential financial data...', 'confidential')
|
186
|
-
doc2 = DocumentService.create_document(alice, 'Team Meeting Notes', 'Weekly standup notes...', 'private')
|
187
|
-
doc3 = DocumentService.create_document(bob, 'Project Proposal', 'New feature proposal...', 'public')
|
188
|
-
|
189
|
-
# Share documents with different permission levels
|
190
|
-
puts "\n📄 Document Sharing:"
|
191
|
-
DocumentService.share_document(doc1, bob, :reviewer) # Bob can review financial report
|
192
|
-
DocumentService.share_document(doc1, charlie, :viewer) # Charlie can only view
|
193
|
-
|
194
|
-
DocumentService.share_document(doc2, bob, :editor) # Bob can edit meeting notes
|
195
|
-
DocumentService.share_document(doc2, charlie, :commenter) # Charlie can comment
|
196
|
-
|
197
|
-
DocumentService.share_document(doc3, alice, :admin) # Alice gets admin on Bob's doc
|
198
|
-
DocumentService.share_document(doc3, charlie, :editor) # Charlie can edit proposal
|
199
|
-
|
200
|
-
# Test permission checks
|
201
|
-
puts "\n🔐 Permission Testing:"
|
202
|
-
puts "Can Bob edit financial report? #{DocumentService.can_user_perform?(bob, doc1, :edit)}"
|
203
|
-
puts "Can Bob delete financial report? #{DocumentService.can_user_perform?(bob, doc1, :delete)}"
|
204
|
-
puts "Can Charlie comment on meeting notes? #{DocumentService.can_user_perform?(charlie, doc2, :comment)}"
|
205
|
-
puts "Can Charlie edit project proposal? #{DocumentService.can_user_perform?(charlie, doc3, :edit)}"
|
206
|
-
|
207
|
-
# Advanced analytics
|
208
|
-
puts "\n📊 Document Analytics:"
|
209
|
-
analytics = doc1.access_analytics
|
210
|
-
puts "Financial Report - Active Users: #{analytics[:active_users].size}"
|
211
|
-
puts "Total Collaborators: #{analytics[:total_collaborators]}"
|
212
|
-
puts 'Permission Breakdown:'
|
213
|
-
analytics[:permission_breakdown].each do |user_id, perms|
|
214
|
-
puts " #{user_id}: #{perms.join(', ')}"
|
215
|
-
end
|
216
|
-
|
217
|
-
# Demonstrate bit encoding efficiency
|
218
|
-
puts "\n⚡ Bit Encoding Efficiency:"
|
219
|
-
score = Familia::Features::Relationships::ScoreEncoding.encode_score(Time.now, %i[read write edit delete])
|
220
|
-
decoded = Familia::Features::Relationships::ScoreEncoding.decode_score(score)
|
221
|
-
puts "Encoded score: #{score}"
|
222
|
-
puts "Decoded permissions: #{decoded[:permission_list].join(', ')}"
|
223
|
-
puts "Permission bits: #{decoded[:permissions]} (#{decoded[:permissions].to_s(2).rjust(8, '0')})"
|
224
|
-
|
225
|
-
# Cleanup
|
226
|
-
puts "\n🧹 Cleanup:"
|
227
|
-
[alice, bob, charlie].each { |user| user.documents.clear }
|
228
|
-
[doc1, doc2, doc3].each do |doc|
|
229
|
-
doc.clear_all_permissions
|
230
|
-
doc.collaborators.clear
|
231
|
-
end
|
232
|
-
|
233
|
-
puts '✅ Integration example completed successfully!'
|
234
|
-
puts "\nThis demonstrates:"
|
235
|
-
puts '• Fine-grained permission management with 8-bit encoding'
|
236
|
-
puts '• Role-based access control with business logic'
|
237
|
-
puts '• Time-based analytics and audit trails'
|
238
|
-
puts '• Efficient Redis storage with sorted sets'
|
239
|
-
puts '• Production-ready error handling and validation'
|
240
|
-
end
|
@@ -1,437 +0,0 @@
|
|
1
|
-
# lib/familia/features/relationships/cascading.rb
|
2
|
-
|
3
|
-
module Familia
|
4
|
-
module Features
|
5
|
-
module Relationships
|
6
|
-
# Cascading module for handling cascade operations during object lifecycle
|
7
|
-
# Supports multi-presence scenarios where objects exist in multiple collections
|
8
|
-
module Cascading
|
9
|
-
# Cascade strategies
|
10
|
-
STRATEGIES = {
|
11
|
-
remove: :remove_from_collections,
|
12
|
-
ignore: :ignore_collections,
|
13
|
-
cascade: :cascade_destroy_dependents
|
14
|
-
}.freeze
|
15
|
-
|
16
|
-
# Class-level cascade configurations
|
17
|
-
def self.included(base)
|
18
|
-
base.extend ClassMethods
|
19
|
-
base.include InstanceMethods
|
20
|
-
super
|
21
|
-
end
|
22
|
-
|
23
|
-
module ClassMethods
|
24
|
-
# Get cascade strategies for all relationships
|
25
|
-
def cascade_strategies
|
26
|
-
strategies = {}
|
27
|
-
|
28
|
-
# Collect strategies from tracking relationships
|
29
|
-
if respond_to?(:tracking_relationships)
|
30
|
-
tracking_relationships.each do |config|
|
31
|
-
key = "#{config[:context_class_name]}.#{config[:collection_name]}"
|
32
|
-
strategies[key] = {
|
33
|
-
type: :tracking,
|
34
|
-
strategy: config[:on_destroy] || :remove,
|
35
|
-
config: config
|
36
|
-
}
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# Collect strategies from membership relationships
|
41
|
-
if respond_to?(:membership_relationships)
|
42
|
-
membership_relationships.each do |config|
|
43
|
-
key = "#{config[:owner_class_name]}.#{config[:collection_name]}"
|
44
|
-
strategies[key] = {
|
45
|
-
type: :membership,
|
46
|
-
strategy: config[:on_destroy] || :remove,
|
47
|
-
config: config
|
48
|
-
}
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# Collect strategies from indexing relationships
|
53
|
-
if respond_to?(:indexing_relationships)
|
54
|
-
indexing_relationships.each do |config|
|
55
|
-
key = if config[:context_class_name] == 'global'
|
56
|
-
"global.#{config[:index_name]}"
|
57
|
-
else
|
58
|
-
"#{config[:context_class_name]}.#{config[:index_name]}"
|
59
|
-
end
|
60
|
-
strategies[key] = {
|
61
|
-
type: :indexing,
|
62
|
-
strategy: :remove, # Indexes should always be cleaned up
|
63
|
-
config: config
|
64
|
-
}
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
strategies
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# Instance methods for cascade operations
|
73
|
-
module InstanceMethods
|
74
|
-
# Execute cascade operations during destroy
|
75
|
-
def execute_cascade_operations
|
76
|
-
strategies = self.class.cascade_strategies
|
77
|
-
|
78
|
-
# Group operations by strategy for efficient execution
|
79
|
-
remove_operations = []
|
80
|
-
cascade_operations = []
|
81
|
-
|
82
|
-
strategies.each_value do |strategy_info|
|
83
|
-
case strategy_info[:strategy]
|
84
|
-
when :remove
|
85
|
-
remove_operations << strategy_info
|
86
|
-
when :cascade
|
87
|
-
cascade_operations << strategy_info
|
88
|
-
when :ignore
|
89
|
-
# Do nothing
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
# Execute remove operations first (cleanup this object's presence)
|
94
|
-
execute_remove_operations(remove_operations) if remove_operations.any?
|
95
|
-
|
96
|
-
# Then execute cascade operations (may trigger other destroys)
|
97
|
-
execute_cascade_operations_recursive(cascade_operations) if cascade_operations.any?
|
98
|
-
end
|
99
|
-
|
100
|
-
# Remove this object from all collections without cascading
|
101
|
-
def remove_from_all_collections
|
102
|
-
strategies = self.class.cascade_strategies
|
103
|
-
remove_operations = strategies.values.reject { |s| s[:strategy] == :ignore }
|
104
|
-
execute_remove_operations(remove_operations)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Check if destroying this object would trigger cascades
|
108
|
-
def cascade_impact
|
109
|
-
strategies = self.class.cascade_strategies
|
110
|
-
impact = {
|
111
|
-
removals: 0,
|
112
|
-
cascades: 0,
|
113
|
-
affected_collections: [],
|
114
|
-
cascade_targets: []
|
115
|
-
}
|
116
|
-
|
117
|
-
strategies.each do |key, strategy_info|
|
118
|
-
case strategy_info[:strategy]
|
119
|
-
when :remove
|
120
|
-
impact[:removals] += 1
|
121
|
-
impact[:affected_collections] << key
|
122
|
-
when :cascade
|
123
|
-
impact[:cascades] += 1
|
124
|
-
impact[:affected_collections] << key
|
125
|
-
|
126
|
-
# Estimate cascade targets (this is expensive, use carefully)
|
127
|
-
targets = estimate_cascade_targets(strategy_info)
|
128
|
-
impact[:cascade_targets].concat(targets)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
impact
|
133
|
-
end
|
134
|
-
|
135
|
-
private
|
136
|
-
|
137
|
-
# Execute removal operations atomically
|
138
|
-
def execute_remove_operations(remove_operations)
|
139
|
-
return if remove_operations.empty?
|
140
|
-
|
141
|
-
dbclient.pipelined do |pipeline|
|
142
|
-
remove_operations.each do |operation|
|
143
|
-
case operation[:type]
|
144
|
-
when :tracking
|
145
|
-
remove_from_tracking_collections(pipeline, operation[:config])
|
146
|
-
when :membership
|
147
|
-
remove_from_membership_collections(pipeline, operation[:config])
|
148
|
-
when :indexing
|
149
|
-
remove_from_indexing_collections(pipeline, operation[:config])
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
# Remove from tracking collections
|
156
|
-
def remove_from_tracking_collections(pipeline, config)
|
157
|
-
context_class_name = config[:context_class_name]
|
158
|
-
collection_name = config[:collection_name]
|
159
|
-
|
160
|
-
# Find all collections this object is tracked in
|
161
|
-
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
162
|
-
|
163
|
-
dbclient.scan_each(match: pattern) do |key|
|
164
|
-
pipeline.zrem(key, identifier)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
# Remove from membership collections
|
169
|
-
def remove_from_membership_collections(pipeline, config)
|
170
|
-
owner_class_name = config[:owner_class_name]
|
171
|
-
collection_name = config[:collection_name]
|
172
|
-
type = config[:type]
|
173
|
-
|
174
|
-
# Find all collections this object is a member of
|
175
|
-
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
176
|
-
|
177
|
-
dbclient.scan_each(match: pattern) do |key|
|
178
|
-
case type
|
179
|
-
when :sorted_set
|
180
|
-
pipeline.zrem(key, identifier)
|
181
|
-
when :set
|
182
|
-
pipeline.srem(key, identifier)
|
183
|
-
when :list
|
184
|
-
pipeline.lrem(key, 0, identifier)
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
# Remove from indexing collections
|
190
|
-
def remove_from_indexing_collections(pipeline, config)
|
191
|
-
context_class_name = config[:context_class_name]
|
192
|
-
index_name = config[:index_name]
|
193
|
-
field = config[:field]
|
194
|
-
|
195
|
-
field_value = send(field) if respond_to?(field)
|
196
|
-
return unless field_value
|
197
|
-
|
198
|
-
if context_class_name == 'global'
|
199
|
-
index_key = "global:#{index_name}"
|
200
|
-
pipeline.hdel(index_key, field_value.to_s)
|
201
|
-
else
|
202
|
-
# Find all indexes this object appears in
|
203
|
-
pattern = "#{context_class_name.downcase}:*:#{index_name}"
|
204
|
-
|
205
|
-
dbclient.scan_each(match: pattern) do |key|
|
206
|
-
pipeline.hdel(key, field_value.to_s)
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
# Execute cascade operations that may trigger dependent destroys
|
212
|
-
def execute_cascade_operations_recursive(cascade_operations)
|
213
|
-
cascade_operations.each do |operation|
|
214
|
-
case operation[:type]
|
215
|
-
when :tracking
|
216
|
-
cascade_tracking_dependents(operation[:config])
|
217
|
-
when :membership
|
218
|
-
cascade_membership_dependents(operation[:config])
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
# Cascade destroy for tracking relationships
|
224
|
-
def cascade_tracking_dependents(config)
|
225
|
-
# This is a complex operation that depends on the specific business logic
|
226
|
-
# For now, we'll provide a framework that can be customized
|
227
|
-
|
228
|
-
context_class_name = config[:context_class_name]
|
229
|
-
collection_name = config[:collection_name]
|
230
|
-
|
231
|
-
# Find all contexts that track this object
|
232
|
-
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
233
|
-
|
234
|
-
dbclient.scan_each(match: pattern) do |key|
|
235
|
-
# Check if this object is the only member
|
236
|
-
if dbclient.zcard(key) == 1 && dbclient.zscore(key, identifier)
|
237
|
-
context_id = key.split(':')[1]
|
238
|
-
|
239
|
-
# Optionally destroy the context if it becomes empty
|
240
|
-
# This is application-specific logic
|
241
|
-
trigger_cascade_callback(:tracking, context_class_name, context_id, collection_name)
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
# Cascade destroy for membership relationships
|
247
|
-
def cascade_membership_dependents(config)
|
248
|
-
# Similar to tracking, this depends on business logic
|
249
|
-
|
250
|
-
owner_class_name = config[:owner_class_name]
|
251
|
-
collection_name = config[:collection_name]
|
252
|
-
type = config[:type]
|
253
|
-
|
254
|
-
# Find all owners that contain this object
|
255
|
-
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
256
|
-
|
257
|
-
dbclient.scan_each(match: pattern) do |key|
|
258
|
-
# Check if this object exists in the collection
|
259
|
-
is_member = case type
|
260
|
-
when :sorted_set
|
261
|
-
dbclient.zscore(key, identifier) != nil
|
262
|
-
when :set
|
263
|
-
dbclient.sismember(key, identifier)
|
264
|
-
when :list
|
265
|
-
dbclient.lpos(key, identifier) != nil
|
266
|
-
end
|
267
|
-
|
268
|
-
if is_member
|
269
|
-
owner_id = key.split(':')[1]
|
270
|
-
trigger_cascade_callback(:membership, owner_class_name, owner_id, collection_name)
|
271
|
-
end
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
# Trigger application-specific cascade callbacks
|
276
|
-
def trigger_cascade_callback(relationship_type, class_name, object_id, collection_name)
|
277
|
-
# This method can be overridden by applications to implement
|
278
|
-
# custom cascade logic
|
279
|
-
|
280
|
-
callback_method = "on_cascade_#{relationship_type}_#{collection_name}"
|
281
|
-
|
282
|
-
return unless respond_to?(callback_method, true)
|
283
|
-
|
284
|
-
send(callback_method, class_name, object_id)
|
285
|
-
end
|
286
|
-
|
287
|
-
# Estimate objects that would be affected by cascading (expensive operation)
|
288
|
-
def estimate_cascade_targets(strategy_info)
|
289
|
-
targets = []
|
290
|
-
|
291
|
-
case strategy_info[:type]
|
292
|
-
when :tracking
|
293
|
-
config = strategy_info[:config]
|
294
|
-
context_class_name = config[:context_class_name]
|
295
|
-
collection_name = config[:collection_name]
|
296
|
-
|
297
|
-
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
298
|
-
dbclient.scan_each(match: pattern) do |key|
|
299
|
-
if dbclient.zscore(key, identifier)
|
300
|
-
context_id = key.split(':')[1]
|
301
|
-
targets << {
|
302
|
-
type: :context,
|
303
|
-
class: context_class_name,
|
304
|
-
id: context_id,
|
305
|
-
collection: collection_name
|
306
|
-
}
|
307
|
-
end
|
308
|
-
end
|
309
|
-
|
310
|
-
when :membership
|
311
|
-
config = strategy_info[:config]
|
312
|
-
owner_class_name = config[:owner_class_name]
|
313
|
-
collection_name = config[:collection_name]
|
314
|
-
type = config[:type]
|
315
|
-
|
316
|
-
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
317
|
-
dbclient.scan_each(match: pattern) do |key|
|
318
|
-
is_member = case type
|
319
|
-
when :sorted_set
|
320
|
-
dbclient.zscore(key, identifier) != nil
|
321
|
-
when :set
|
322
|
-
dbclient.sismember(key, identifier)
|
323
|
-
when :list
|
324
|
-
dbclient.lpos(key, identifier) != nil
|
325
|
-
end
|
326
|
-
|
327
|
-
if is_member
|
328
|
-
owner_id = key.split(':')[1]
|
329
|
-
targets << {
|
330
|
-
type: :owner,
|
331
|
-
class: owner_class_name,
|
332
|
-
id: owner_id,
|
333
|
-
collection: collection_name
|
334
|
-
}
|
335
|
-
end
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
targets
|
340
|
-
end
|
341
|
-
|
342
|
-
# Dry run cascade operations (for testing/preview)
|
343
|
-
def cascade_dry_run
|
344
|
-
strategies = self.class.cascade_strategies
|
345
|
-
|
346
|
-
preview = {
|
347
|
-
removals: [],
|
348
|
-
cascades: [],
|
349
|
-
affected_keys: []
|
350
|
-
}
|
351
|
-
|
352
|
-
strategies.each do |key, strategy_info|
|
353
|
-
case strategy_info[:strategy]
|
354
|
-
when :remove
|
355
|
-
affected_keys = find_affected_keys(strategy_info)
|
356
|
-
preview[:removals] << {
|
357
|
-
relationship: key,
|
358
|
-
keys: affected_keys,
|
359
|
-
count: affected_keys.length
|
360
|
-
}
|
361
|
-
preview[:affected_keys].concat(affected_keys)
|
362
|
-
|
363
|
-
when :cascade
|
364
|
-
cascade_targets = estimate_cascade_targets(strategy_info)
|
365
|
-
preview[:cascades] << {
|
366
|
-
relationship: key,
|
367
|
-
targets: cascade_targets,
|
368
|
-
count: cascade_targets.length
|
369
|
-
}
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
preview[:affected_keys].uniq!
|
374
|
-
preview
|
375
|
-
end
|
376
|
-
|
377
|
-
# Find all Redis keys that would be affected by removing this object
|
378
|
-
def find_affected_keys(strategy_info)
|
379
|
-
affected_keys = []
|
380
|
-
|
381
|
-
case strategy_info[:type]
|
382
|
-
when :tracking
|
383
|
-
config = strategy_info[:config]
|
384
|
-
context_class_name = config[:context_class_name]
|
385
|
-
collection_name = config[:collection_name]
|
386
|
-
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
387
|
-
|
388
|
-
dbclient.scan_each(match: pattern) do |key|
|
389
|
-
affected_keys << key if dbclient.zscore(key, identifier)
|
390
|
-
end
|
391
|
-
|
392
|
-
when :membership
|
393
|
-
config = strategy_info[:config]
|
394
|
-
owner_class_name = config[:owner_class_name]
|
395
|
-
collection_name = config[:collection_name]
|
396
|
-
type = config[:type]
|
397
|
-
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
398
|
-
|
399
|
-
dbclient.scan_each(match: pattern) do |key|
|
400
|
-
is_member = case type
|
401
|
-
when :sorted_set
|
402
|
-
dbclient.zscore(key, identifier) != nil
|
403
|
-
when :set
|
404
|
-
dbclient.sismember(key, identifier)
|
405
|
-
when :list
|
406
|
-
dbclient.lpos(key, identifier) != nil
|
407
|
-
end
|
408
|
-
affected_keys << key if is_member
|
409
|
-
end
|
410
|
-
|
411
|
-
when :indexing
|
412
|
-
config = strategy_info[:config]
|
413
|
-
context_class_name = config[:context_class_name]
|
414
|
-
index_name = config[:index_name]
|
415
|
-
field = config[:field]
|
416
|
-
|
417
|
-
field_value = send(field) if respond_to?(field)
|
418
|
-
if field_value
|
419
|
-
if context_class_name == 'global'
|
420
|
-
index_key = "global:#{index_name}"
|
421
|
-
affected_keys << index_key if dbclient.hexists(index_key, field_value.to_s)
|
422
|
-
else
|
423
|
-
pattern = "#{context_class_name.downcase}:*:#{index_name}"
|
424
|
-
dbclient.scan_each(match: pattern) do |key|
|
425
|
-
affected_keys << key if dbclient.hexists(key, field_value.to_s)
|
426
|
-
end
|
427
|
-
end
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
|
-
affected_keys
|
432
|
-
end
|
433
|
-
end
|
434
|
-
end
|
435
|
-
end
|
436
|
-
end
|
437
|
-
end
|