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
@@ -0,0 +1,1365 @@
|
|
1
|
+
# Familia v2.0.0-pre Series Technical Reference
|
2
|
+
|
3
|
+
**Familia** is a Ruby ORM for Valkey/Redis providing object mapping, relationships, and advanced features like encryption, connection pooling, and permission systems. This technical reference covers the major classes, methods, and usage patterns introduced in the v2.0.0-pre series.
|
4
|
+
|
5
|
+
> For conceptual understanding and getting started with Familia, see the [Overview Guide](../overview.md). This document provides detailed implementation patterns and advanced configuration options.
|
6
|
+
|
7
|
+
---
|
8
|
+
|
9
|
+
## Core Architecture
|
10
|
+
|
11
|
+
### Base Classes
|
12
|
+
|
13
|
+
#### `Familia::Horreum` - Primary ORM Base Class
|
14
|
+
The main base class for Valkey/Redis-backed objects, similar to ActiveRecord models.
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
class User < Familia::Horreum
|
18
|
+
# Basic field definitions
|
19
|
+
field :name, :email, :created_at
|
20
|
+
|
21
|
+
# Valkey/Redis data types as instance variables
|
22
|
+
list :sessions # Valkey/Redis list
|
23
|
+
set :tags # Valkey/Redis set
|
24
|
+
zset :scores # Valkey/Redis sorted set
|
25
|
+
hashkey :settings # Valkey/Redis hash
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
**Key Methods:**
|
30
|
+
- `save` - Persist object to Valkey/Redis
|
31
|
+
- `save_if_not_exists` - Conditional persistence (v2.0.0-pre6)
|
32
|
+
- `load` - Load object from Valkey/Redis
|
33
|
+
- `exists?` - Check if object exists in Valkey/Redis
|
34
|
+
- `destroy` - Remove object from Valkey/Redis
|
35
|
+
|
36
|
+
#### `Familia::DataType` - Valkey/Redis Data Type Wrapper
|
37
|
+
Base class for Valkey/Redis data type implementations.
|
38
|
+
|
39
|
+
**Registered Types:**
|
40
|
+
- `String` - Valkey/Redis strings
|
41
|
+
- `List` - Valkey/Redis lists
|
42
|
+
- `Set` - Valkey/Redis sets
|
43
|
+
- `SortedSet` - Valkey/Redis sorted sets
|
44
|
+
- `HashKey` - Valkey/Redis hashes
|
45
|
+
- `Counter` - Atomic counters
|
46
|
+
- `Lock` - Distributed locks
|
47
|
+
|
48
|
+
---
|
49
|
+
|
50
|
+
## Feature System (v2.0.0-pre5+)
|
51
|
+
|
52
|
+
### Feature Architecture
|
53
|
+
Modular system for extending Horreum classes with reusable functionality.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class Customer < Familia::Horreum
|
57
|
+
feature :expiration # TTL management
|
58
|
+
feature :safe_dump # API-safe serialization
|
59
|
+
feature :encrypted_fields # Field encryption
|
60
|
+
feature :transient_fields # Non-persistent fields
|
61
|
+
feature :relationships # Object relationships
|
62
|
+
|
63
|
+
field :name, :email
|
64
|
+
encrypted_field :api_key
|
65
|
+
transient_field :password
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Built-in Features
|
70
|
+
|
71
|
+
#### 1. Expiration Feature
|
72
|
+
TTL (Time To Live) management with cascading expiration.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class Session < Familia::Horreum
|
76
|
+
feature :expiration
|
77
|
+
|
78
|
+
field :user_id, :token
|
79
|
+
default_expiration 24.hours
|
80
|
+
|
81
|
+
# Cascade expiration to related objects
|
82
|
+
cascade_expiration_to :user_activity
|
83
|
+
end
|
84
|
+
|
85
|
+
session = Session.new(user_id: 123, token: "abc123")
|
86
|
+
session.save
|
87
|
+
session.expire_in(1.hour) # UnsortedSet custom expiration
|
88
|
+
session.ttl # Check remaining time
|
89
|
+
```
|
90
|
+
|
91
|
+
#### 2. SafeDump Feature
|
92
|
+
API-safe serialization excluding sensitive fields.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class User < Familia::Horreum
|
96
|
+
feature :safe_dump
|
97
|
+
|
98
|
+
field :name, :email, :password_hash
|
99
|
+
safe_dump_field :name, :email # Only these fields in safe_dump
|
100
|
+
end
|
101
|
+
|
102
|
+
user = User.new(name: "Alice", email: "alice@example.com", password_hash: "secret")
|
103
|
+
user.safe_dump # => {"name" => "Alice", "email" => "alice@example.com"}
|
104
|
+
```
|
105
|
+
|
106
|
+
#### 3. Encrypted Fields Feature (v2.0.0-pre5)
|
107
|
+
Transparent field-level encryption with multiple providers.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# Configuration
|
111
|
+
Familia.configure do |config|
|
112
|
+
config.encryption_keys = {
|
113
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY'],
|
114
|
+
v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
|
115
|
+
}
|
116
|
+
config.current_key_version = :v2
|
117
|
+
end
|
118
|
+
|
119
|
+
class Vault < Familia::Horreum
|
120
|
+
feature :encrypted_fields
|
121
|
+
|
122
|
+
field :name # Plaintext
|
123
|
+
encrypted_field :secret_key # Encrypted with XChaCha20-Poly1305
|
124
|
+
encrypted_field :api_token # Field-specific key derivation
|
125
|
+
encrypted_field :private_data # Transparent access
|
126
|
+
end
|
127
|
+
|
128
|
+
vault = Vault.new(
|
129
|
+
name: "Production Secrets",
|
130
|
+
secret_key: "super-secret-123",
|
131
|
+
api_token: "sk-1234567890"
|
132
|
+
)
|
133
|
+
vault.save
|
134
|
+
|
135
|
+
# Transparent access - automatically encrypted/decrypted
|
136
|
+
vault.secret_key # => "super-secret-123" (decrypted on access)
|
137
|
+
```
|
138
|
+
|
139
|
+
**Encryption Providers:**
|
140
|
+
- **XChaCha20-Poly1305** (preferred) - Requires `rbnacl` gem
|
141
|
+
- **AES-256-GCM** (fallback) - Uses OpenSSL, no dependencies
|
142
|
+
|
143
|
+
**Advanced Security Implementation:**
|
144
|
+
```ruby
|
145
|
+
# Multiple encryption providers with fallback
|
146
|
+
class CriticalData < Familia::Horreum
|
147
|
+
feature :encrypted_fields
|
148
|
+
|
149
|
+
# Configure encryption provider preference
|
150
|
+
set_encryption_provider :xchacha20_poly1305 # Preferred
|
151
|
+
set_fallback_provider :aes_gcm # Fallback
|
152
|
+
|
153
|
+
encrypted_field :credit_card, aad_fields: [:user_id, :created_at]
|
154
|
+
encrypted_field :ssn, provider: :xchacha20_poly1305 # Force specific provider
|
155
|
+
encrypted_field :notes # Uses default provider
|
156
|
+
end
|
157
|
+
|
158
|
+
# Key versioning and rotation
|
159
|
+
Familia.configure do |config|
|
160
|
+
config.encryption_keys = {
|
161
|
+
v1: ENV['OLD_KEY'], # Legacy key
|
162
|
+
v2: ENV['CURRENT_KEY'], # Current key
|
163
|
+
v3: ENV['NEW_KEY'] # New key for rotation
|
164
|
+
}
|
165
|
+
config.current_key_version = :v2
|
166
|
+
|
167
|
+
# Provider configuration
|
168
|
+
config.encryption_providers = {
|
169
|
+
xchacha20_poly1305: {
|
170
|
+
key_size: 32,
|
171
|
+
nonce_size: 24,
|
172
|
+
require_gem: 'rbnacl'
|
173
|
+
},
|
174
|
+
aes_gcm: {
|
175
|
+
key_size: 32,
|
176
|
+
iv_size: 12,
|
177
|
+
tag_size: 16
|
178
|
+
}
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
# Request-level key caching for performance
|
183
|
+
Familia::Encryption.with_request_cache do
|
184
|
+
1000.times do |i|
|
185
|
+
record = CriticalData.new(
|
186
|
+
credit_card: "4111-1111-1111-#{i.to_s.rjust(4, '0')}",
|
187
|
+
ssn: "123-45-#{i.to_s.rjust(4, '0')}",
|
188
|
+
notes: "Customer record #{i}"
|
189
|
+
)
|
190
|
+
record.save # Reuses derived keys for performance
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Key rotation procedures
|
195
|
+
CriticalData.all.each do |record|
|
196
|
+
# Re-encrypt with current key version
|
197
|
+
record.re_encrypt_fields!
|
198
|
+
|
199
|
+
# Verify encryption status
|
200
|
+
status = record.encrypted_fields_status
|
201
|
+
puts "Record #{record.identifier}: #{status}"
|
202
|
+
# => {credit_card: {encrypted: true, key_version: :v2, provider: :xchacha20_poly1305}}
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
**ConcealedString Security Features:**
|
207
|
+
```ruby
|
208
|
+
# Automatic protection against accidental exposure
|
209
|
+
user = CriticalData.load("user123")
|
210
|
+
|
211
|
+
# Safe operations
|
212
|
+
user.credit_card.class # => ConcealedString
|
213
|
+
user.credit_card.to_s # => "[CONCEALED]"
|
214
|
+
user.credit_card.inspect # => "[CONCEALED]"
|
215
|
+
user.credit_card.to_json # => "\"[CONCEALED]\""
|
216
|
+
|
217
|
+
# Explicit access when needed
|
218
|
+
actual_card = user.credit_card.reveal # => "4111-1111-1111-1234"
|
219
|
+
|
220
|
+
# Logging safety
|
221
|
+
Rails.logger.info "Processing card: #{user.credit_card}"
|
222
|
+
# => "Processing card: [CONCEALED]" (safe for logs)
|
223
|
+
|
224
|
+
# JSON serialization safety
|
225
|
+
user_data = user.to_json
|
226
|
+
# All encrypted fields show as "[CONCEALED]" in JSON
|
227
|
+
```
|
228
|
+
|
229
|
+
#### 4. Transient Fields Feature (v2.0.0-pre5)
|
230
|
+
Non-persistent fields with memory-safe handling.
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
class LoginForm < Familia::Horreum
|
234
|
+
feature :transient_fields
|
235
|
+
|
236
|
+
field :username # Persistent
|
237
|
+
transient_field :password # Never stored in Redis
|
238
|
+
transient_field :csrf_token # Runtime only
|
239
|
+
end
|
240
|
+
|
241
|
+
form = LoginForm.new(username: "alice", password: "secret123")
|
242
|
+
form.save # Only saves 'username', password is transient
|
243
|
+
|
244
|
+
form.password.class # => Familia::Features::TransientFields::RedactedString
|
245
|
+
form.password.to_s # => "[REDACTED]" (safe for logging)
|
246
|
+
form.password.reveal # => "secret123" (explicit access)
|
247
|
+
```
|
248
|
+
|
249
|
+
**RedactedString** - Security wrapper preventing accidental exposure:
|
250
|
+
- `to_s` returns "[REDACTED]"
|
251
|
+
- `inspect` returns "[REDACTED]"
|
252
|
+
- `reveal` method for explicit access
|
253
|
+
- Safe for logging and serialization
|
254
|
+
|
255
|
+
#### 5. Relationships Feature (v2.0.0-pre7)
|
256
|
+
Comprehensive object relationship system with automatic management, clean Ruby-idiomatic syntax, and simplified method generation.
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
class Customer < Familia::Horreum
|
260
|
+
feature :relationships
|
261
|
+
|
262
|
+
identifier_field :custid
|
263
|
+
field :custid, :name, :email
|
264
|
+
|
265
|
+
# Collections for storing related object IDs
|
266
|
+
set :domains # Simple set
|
267
|
+
zset :activity # Scored/sorted collection
|
268
|
+
|
269
|
+
# Class-level indexed lookups (automatically managed on save/destroy)
|
270
|
+
class_indexed_by :email, :email_lookup
|
271
|
+
|
272
|
+
# Class-level tracking with scoring (automatically managed on save/destroy)
|
273
|
+
class_participates_in :all_customers, score: :created_at
|
274
|
+
end
|
275
|
+
|
276
|
+
class Domain < Familia::Horreum
|
277
|
+
feature :relationships
|
278
|
+
|
279
|
+
identifier_field :domain_id
|
280
|
+
field :domain_id, :name, :status
|
281
|
+
|
282
|
+
# Bidirectional membership with clean << operator support
|
283
|
+
participates_in Customer, :domains
|
284
|
+
|
285
|
+
# Relationship-scoped indexing (per-customer domain lookups)
|
286
|
+
indexed_by :name, :domain_index, target: Customer
|
287
|
+
|
288
|
+
# Class-level conditional tracking with lambda scoring
|
289
|
+
class_participates_in :active_domains,
|
290
|
+
score: -> { status == 'active' ? Familia.now.to_i : 0 }
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
**Relationship Operations with Automatic Management:**
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
# Create and save objects (automatic indexing and tracking)
|
298
|
+
customer = Customer.new(custid: "cust123", name: "Acme Corp", email: "admin@acme.com")
|
299
|
+
customer.save # Automatically adds to email_lookup and all_customers
|
300
|
+
|
301
|
+
domain = Domain.new(domain_id: "dom456", name: "acme.com", status: "active")
|
302
|
+
domain.save # Automatically adds to active_domains
|
303
|
+
|
304
|
+
# Establish relationships using clean Ruby-like << operator
|
305
|
+
customer.domains << domain # Clean, Ruby-idiomatic collection syntax
|
306
|
+
|
307
|
+
# Query relationships
|
308
|
+
domain.in_customer_domains?(customer.custid) # => true
|
309
|
+
customer.domains.member?(domain.identifier) # => true
|
310
|
+
|
311
|
+
# O(1) indexed lookups (automatic management - no manual calls needed)
|
312
|
+
found_id = Customer.email_lookup.get("admin@acme.com")
|
313
|
+
found_customer = Customer.find_by_email("admin@acme.com") # Convenience method
|
314
|
+
|
315
|
+
# Relationship-scoped lookups
|
316
|
+
customer_domain = customer.find_by_name("acme.com") # Find within customer
|
317
|
+
|
318
|
+
# Class-level tracking queries
|
319
|
+
recent_customers = Customer.all_customers.range_by_score(
|
320
|
+
(Familia.now - 24.hours).to_i, '+inf'
|
321
|
+
)
|
322
|
+
active_domains = Domain.active_domains.members
|
323
|
+
```
|
324
|
+
|
325
|
+
**Key Features:**
|
326
|
+
- **Automatic Management**: Objects automatically added/removed from class-level collections on save/destroy
|
327
|
+
- **Clean Syntax**: Collections support Ruby-like `customer.domains << domain` syntax
|
328
|
+
- **Simplified Methods**: No complex "global" terminology - uses clear `class_` prefixes
|
329
|
+
- **Performance**: O(1) hash lookups and efficient sorted set operations
|
330
|
+
- **Flexibility**: Supports class-level and relationship-scoped indexing patterns
|
331
|
+
|
332
|
+
#### 6. Object Identifier Feature (v2.0.0-pre7)
|
333
|
+
Automatic generation of unique object identifiers with configurable strategies.
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
class Document < Familia::Horreum
|
337
|
+
feature :object_identifier, generator: :uuid_v4
|
338
|
+
|
339
|
+
field :title, :content, :created_at
|
340
|
+
end
|
341
|
+
|
342
|
+
class Session < Familia::Horreum
|
343
|
+
feature :object_identifier, generator: :hex, length: 16
|
344
|
+
|
345
|
+
field :user_id, :data, :expires_at
|
346
|
+
end
|
347
|
+
|
348
|
+
class ApiKey < Familia::Horreum
|
349
|
+
feature :object_identifier, generator: :custom
|
350
|
+
|
351
|
+
field :name, :permissions, :created_at
|
352
|
+
|
353
|
+
# Custom generator implementation
|
354
|
+
def self.generate_identifier
|
355
|
+
"ak_#{SecureRandom.alphanumeric(32)}"
|
356
|
+
end
|
357
|
+
end
|
358
|
+
```
|
359
|
+
|
360
|
+
**Generator Types:**
|
361
|
+
- `:uuid_v4` - Standard UUID v4 format (36 characters)
|
362
|
+
- `:hex` - Hexadecimal strings (configurable length, default 12)
|
363
|
+
- `:custom` - User-defined generator method
|
364
|
+
|
365
|
+
**Technical Implementation:**
|
366
|
+
```ruby
|
367
|
+
# Auto-generated on object creation
|
368
|
+
doc = Document.create(title: "My Document")
|
369
|
+
doc.objid # => "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
370
|
+
|
371
|
+
session = Session.create(user_id: "123")
|
372
|
+
session.objid # => "a1b2c3d4e5f67890"
|
373
|
+
|
374
|
+
# Custom identifier validation
|
375
|
+
api_key = ApiKey.create(name: "Production API")
|
376
|
+
api_key.objid # => "ak_Xy9ZaBcD3fG8HjKlMnOpQrStUvWxYz12"
|
377
|
+
|
378
|
+
# Collision detection and retry logic
|
379
|
+
Document.feature_options(:object_identifier)
|
380
|
+
#=> {generator: :uuid_v4, max_retries: 3, collision_check: true}
|
381
|
+
```
|
382
|
+
|
383
|
+
#### 7. External Identifier Feature (v2.0.0-pre7)
|
384
|
+
Integration patterns for external system identifiers with validation and mapping.
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
class ExternalUser < Familia::Horreum
|
388
|
+
feature :external_identifier
|
389
|
+
|
390
|
+
identifier_field :internal_id
|
391
|
+
field :internal_id, :external_id, :name, :sync_status, :last_sync_at
|
392
|
+
|
393
|
+
# External system validation (custom implementation)
|
394
|
+
def valid_external_id?
|
395
|
+
external_id.present? && external_id.match?(/^ext_\d{6,}$/)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
class LegacyAccount < Familia::Horreum
|
400
|
+
feature :external_identifier, prefix: "legacy"
|
401
|
+
|
402
|
+
field :legacy_account_id, :migrated_at, :migration_status
|
403
|
+
|
404
|
+
# Custom validation logic
|
405
|
+
def valid_external_id?
|
406
|
+
legacy_account_id.present? &&
|
407
|
+
legacy_account_id.match?(/^LAC[A-Z]{2}\d{8}$/)
|
408
|
+
end
|
409
|
+
|
410
|
+
# Bidirectional mapping
|
411
|
+
def self.find_by_legacy_id(legacy_id)
|
412
|
+
mapping = external_id_mapping.get(legacy_id)
|
413
|
+
mapping ? load(mapping) : nil
|
414
|
+
end
|
415
|
+
end
|
416
|
+
```
|
417
|
+
|
418
|
+
**External ID Management:**
|
419
|
+
```ruby
|
420
|
+
# Create with external mapping
|
421
|
+
user = ExternalUser.new(
|
422
|
+
internal_id: SecureRandom.uuid,
|
423
|
+
external_id: "ext_123456",
|
424
|
+
name: "John Doe"
|
425
|
+
)
|
426
|
+
user.save # Automatically creates bidirectional mapping
|
427
|
+
|
428
|
+
# Lookup by external ID (custom implementation)
|
429
|
+
found_user = nil
|
430
|
+
ExternalUser.all.each do |user|
|
431
|
+
if user.external_id == "ext_123456"
|
432
|
+
found_user = user
|
433
|
+
break
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
# Sync status tracking (custom implementation)
|
438
|
+
user.sync_status = "pending"
|
439
|
+
user.save
|
440
|
+
user.sync_status = "completed"
|
441
|
+
user.save
|
442
|
+
```
|
443
|
+
|
444
|
+
#### 8. Quantization Feature (v2.0.0-pre7)
|
445
|
+
Advanced time-based data bucketing with configurable strategies and analytics integration.
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
class DailyMetric < Familia::Horreum
|
449
|
+
feature :quantization
|
450
|
+
|
451
|
+
identifier_field :metric_key
|
452
|
+
field :metric_key, :bucket_timestamp, :value_count, :sum_value
|
453
|
+
|
454
|
+
# Example: Basic time quantization
|
455
|
+
string :counter, default_expiration: 1.day, quantize: [10.minutes, '%H:%M']
|
456
|
+
end
|
457
|
+
```
|
458
|
+
|
459
|
+
**Basic Usage:**
|
460
|
+
```ruby
|
461
|
+
# Create metric with quantized timestamp
|
462
|
+
metric = DailyMetric.new(metric_key: "page_views")
|
463
|
+
metric.counter.increment
|
464
|
+
|
465
|
+
# Time-based data grouping occurs automatically
|
466
|
+
# All data within the same 10-minute window shares the same key
|
467
|
+
```
|
468
|
+
|
469
|
+
**Key Benefits:**
|
470
|
+
- **Time Bucketing**: Group time-based data into configurable intervals
|
471
|
+
- **Reduced Storage**: Aggregate similar data points to optimize memory usage
|
472
|
+
- **Analytics Ready**: Perfect for dashboards and time-series data visualization
|
473
|
+
|
474
|
+
---
|
475
|
+
|
476
|
+
## Advanced Feature System Architecture (v2.0.0-pre7)
|
477
|
+
|
478
|
+
### Feature Autoloader for Complex Projects
|
479
|
+
Organize features into modular files for large applications.
|
480
|
+
|
481
|
+
```ruby
|
482
|
+
# app/models/customer.rb - Main model file
|
483
|
+
class Customer < Familia::Horreum
|
484
|
+
module Features
|
485
|
+
include Familia::Features::Autoloader
|
486
|
+
# Automatically loads all .rb files from app/models/customer/features/
|
487
|
+
end
|
488
|
+
|
489
|
+
# Core model definition
|
490
|
+
identifier_field :custid
|
491
|
+
field :custid, :name, :email, :created_at
|
492
|
+
end
|
493
|
+
|
494
|
+
# app/models/customer/features/notifications.rb
|
495
|
+
module Customer::Features::Notifications
|
496
|
+
def send_welcome_email
|
497
|
+
NotificationService.send_template(
|
498
|
+
email: email,
|
499
|
+
template: 'customer_welcome',
|
500
|
+
variables: { name: name, custid: custid }
|
501
|
+
)
|
502
|
+
end
|
503
|
+
|
504
|
+
def send_invoice_reminder(invoice_id)
|
505
|
+
NotificationService.send_template(
|
506
|
+
email: email,
|
507
|
+
template: 'invoice_reminder',
|
508
|
+
variables: { invoice_id: invoice_id }
|
509
|
+
)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
# app/models/customer/features/analytics.rb
|
514
|
+
module Customer::Features::Analytics
|
515
|
+
extend ActiveSupport::Concern
|
516
|
+
|
517
|
+
included do
|
518
|
+
# Add analytics tracking to core model
|
519
|
+
feature :relationships
|
520
|
+
class_participates_in :customer_analytics, score: :created_at
|
521
|
+
end
|
522
|
+
|
523
|
+
def track_activity(activity_type, metadata = {})
|
524
|
+
activity_data = {
|
525
|
+
custid: custid,
|
526
|
+
activity: activity_type,
|
527
|
+
timestamp: Familia.now.to_i,
|
528
|
+
metadata: metadata
|
529
|
+
}
|
530
|
+
|
531
|
+
# Store in customer's activity stream
|
532
|
+
activities.unshift(activity_data.to_json)
|
533
|
+
activities.trim(0, 999) # Keep last 1000 activities
|
534
|
+
end
|
535
|
+
|
536
|
+
def recent_activities(limit = 10)
|
537
|
+
activities.range(0, limit - 1).map { |json| JSON.parse(json) }
|
538
|
+
end
|
539
|
+
end
|
540
|
+
```
|
541
|
+
|
542
|
+
### Feature Dependencies and Loading Order
|
543
|
+
Control feature loading sequence with dependency declarations.
|
544
|
+
|
545
|
+
```ruby
|
546
|
+
# lib/features/advanced_encryption.rb
|
547
|
+
module AdvancedEncryption
|
548
|
+
extend Familia::Features::Autoloadable
|
549
|
+
|
550
|
+
def self.depends_on
|
551
|
+
[:encrypted_fields, :safe_dump] # Required features
|
552
|
+
end
|
553
|
+
|
554
|
+
def self.included(base)
|
555
|
+
base.extend ClassMethods
|
556
|
+
end
|
557
|
+
|
558
|
+
module ClassMethods
|
559
|
+
def encrypt_all_fields!
|
560
|
+
# Batch encrypt all existing records
|
561
|
+
all_records.each(&:re_encrypt_fields!)
|
562
|
+
end
|
563
|
+
|
564
|
+
def encryption_health_check
|
565
|
+
# Validate encryption across all records
|
566
|
+
failed_records = []
|
567
|
+
all_records.each do |record|
|
568
|
+
unless record.encrypted_fields_status.all? { |_, status| status[:encrypted] }
|
569
|
+
failed_records << record.identifier
|
570
|
+
end
|
571
|
+
end
|
572
|
+
failed_records
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
def secure_export
|
577
|
+
# Combine safe_dump with additional security
|
578
|
+
exported = safe_dump
|
579
|
+
exported[:export_timestamp] = Familia.now.to_i
|
580
|
+
exported[:checksum] = Digest::SHA256.hexdigest(exported.to_json)
|
581
|
+
exported
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
# Usage with automatic dependency resolution
|
586
|
+
class SecureCustomer < Familia::Horreum
|
587
|
+
feature :advanced_encryption # Automatically includes dependencies
|
588
|
+
|
589
|
+
field :name, :email
|
590
|
+
encrypted_field :api_key, :private_notes
|
591
|
+
safe_dump_field :name, :email
|
592
|
+
end
|
593
|
+
```
|
594
|
+
|
595
|
+
### Per-Class Feature Configuration Isolation
|
596
|
+
Each class maintains independent feature options.
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
class PrimaryCache < Familia::Horreum
|
600
|
+
feature :expiration, cascade_to: [:secondary_cache]
|
601
|
+
feature :quantization, time_buckets: [1.hour, 6.hours, 1.day]
|
602
|
+
|
603
|
+
field :cache_key, :value, :hit_count
|
604
|
+
default_expiration 24.hours
|
605
|
+
end
|
606
|
+
|
607
|
+
class SecondaryCache < Familia::Horreum
|
608
|
+
feature :expiration, cascade_to: [] # No further cascading
|
609
|
+
feature :quantization, time_buckets: [1.day, 1.week] # Different buckets
|
610
|
+
|
611
|
+
field :cache_key, :backup_value, :backup_timestamp
|
612
|
+
default_expiration 7.days
|
613
|
+
end
|
614
|
+
|
615
|
+
# Feature options are completely isolated
|
616
|
+
PrimaryCache.feature_options(:expiration)
|
617
|
+
#=> {cascade_to: [:secondary_cache]}
|
618
|
+
|
619
|
+
SecondaryCache.feature_options(:expiration)
|
620
|
+
#=> {cascade_to: []}
|
621
|
+
|
622
|
+
PrimaryCache.feature_options(:quantization)
|
623
|
+
#=> {time_buckets: [3600, 21600, 86400]}
|
624
|
+
|
625
|
+
SecondaryCache.feature_options(:quantization)
|
626
|
+
#=> {time_buckets: [86400, 604800]}
|
627
|
+
```
|
628
|
+
|
629
|
+
### Runtime Feature Management
|
630
|
+
Add, remove, and configure features dynamically.
|
631
|
+
|
632
|
+
```ruby
|
633
|
+
class DynamicModel < Familia::Horreum
|
634
|
+
field :name, :status
|
635
|
+
|
636
|
+
def self.enable_feature_set(feature_set)
|
637
|
+
case feature_set
|
638
|
+
when :basic
|
639
|
+
feature :expiration
|
640
|
+
feature :safe_dump
|
641
|
+
when :secure
|
642
|
+
feature :expiration
|
643
|
+
feature :encrypted_fields
|
644
|
+
feature :safe_dump
|
645
|
+
when :analytics
|
646
|
+
feature :expiration
|
647
|
+
feature :relationships
|
648
|
+
feature :quantization
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
def self.feature_enabled?(feature_name)
|
653
|
+
features_enabled.include?(feature_name.to_sym)
|
654
|
+
end
|
655
|
+
|
656
|
+
def self.disable_feature(feature_name)
|
657
|
+
# Remove feature from enabled list (affects new instances)
|
658
|
+
features_enabled.delete(feature_name.to_sym)
|
659
|
+
remove_feature_options(feature_name)
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
# Runtime configuration
|
664
|
+
DynamicModel.enable_feature_set(:analytics)
|
665
|
+
DynamicModel.feature_enabled?(:relationships) # => true
|
666
|
+
|
667
|
+
# Conditional feature usage
|
668
|
+
if DynamicModel.feature_enabled?(:encrypted_fields)
|
669
|
+
DynamicModel.encrypted_field :sensitive_data
|
670
|
+
end
|
671
|
+
```
|
672
|
+
|
673
|
+
---
|
674
|
+
|
675
|
+
## Connection Management (v2.0.0-pre+)
|
676
|
+
|
677
|
+
### Connection Provider Pattern
|
678
|
+
Flexible connection pooling with provider-based architecture.
|
679
|
+
|
680
|
+
```ruby
|
681
|
+
# Basic Valkey/Redis connection
|
682
|
+
Familia.configure do |config|
|
683
|
+
config.redis_uri = "redis://localhost:6379/0"
|
684
|
+
end
|
685
|
+
|
686
|
+
# Connection pooling with ConnectionPool gem
|
687
|
+
require 'connection_pool'
|
688
|
+
|
689
|
+
Familia.connection_provider = lambda do |uri|
|
690
|
+
parsed = URI.parse(uri)
|
691
|
+
pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
|
692
|
+
|
693
|
+
@pools ||= {}
|
694
|
+
@pools[pool_key] ||= ConnectionPool.new(size: 10, timeout: 5) do
|
695
|
+
Redis.new(
|
696
|
+
host: parsed.host,
|
697
|
+
port: parsed.port,
|
698
|
+
db: parsed.db || 0,
|
699
|
+
connect_timeout: 1,
|
700
|
+
read_timeout: 1,
|
701
|
+
write_timeout: 1
|
702
|
+
)
|
703
|
+
end
|
704
|
+
|
705
|
+
@pools[pool_key].with { |conn| yield conn if block_given?; conn }
|
706
|
+
end
|
707
|
+
```
|
708
|
+
|
709
|
+
### Multi-Database Support
|
710
|
+
Configure different logical databases for different models.
|
711
|
+
|
712
|
+
```ruby
|
713
|
+
class User < Familia::Horreum
|
714
|
+
logical_database 0 # Use database 0
|
715
|
+
field :name, :email
|
716
|
+
end
|
717
|
+
|
718
|
+
class Session < Familia::Horreum
|
719
|
+
logical_database 1 # Use database 1
|
720
|
+
field :user_id, :token
|
721
|
+
end
|
722
|
+
|
723
|
+
class Cache < Familia::Horreum
|
724
|
+
logical_database 2 # Use database 2
|
725
|
+
field :key, :value
|
726
|
+
end
|
727
|
+
```
|
728
|
+
|
729
|
+
---
|
730
|
+
|
731
|
+
## Advanced Relationship Patterns
|
732
|
+
|
733
|
+
### Permission-Encoded Relationships (v2.0.0-pre7)
|
734
|
+
Combine timestamps with permission bits for access control.
|
735
|
+
|
736
|
+
```ruby
|
737
|
+
class Document < Familia::Horreum
|
738
|
+
feature :relationships
|
739
|
+
|
740
|
+
identifier_field :doc_id
|
741
|
+
field :doc_id, :title, :content
|
742
|
+
|
743
|
+
# Permission constants (bit flags)
|
744
|
+
READ = 1 # 001
|
745
|
+
WRITE = 2 # 010
|
746
|
+
DELETE = 4 # 100
|
747
|
+
ADMIN = 8 # 1000
|
748
|
+
end
|
749
|
+
|
750
|
+
class UserDocumentAccess
|
751
|
+
# Encode timestamp + permissions into sorted set score
|
752
|
+
def self.encode_score(timestamp, permissions)
|
753
|
+
"#{timestamp}.#{permissions}".to_f
|
754
|
+
end
|
755
|
+
|
756
|
+
def self.decode_score(score)
|
757
|
+
parts = score.to_s.split('.')
|
758
|
+
timestamp = parts[0].to_i
|
759
|
+
permissions = parts[1] ? parts[1].to_i : 0
|
760
|
+
[timestamp, permissions]
|
761
|
+
end
|
762
|
+
|
763
|
+
# Check if user has specific permission
|
764
|
+
def self.has_permission?(permissions, required)
|
765
|
+
(permissions & required) != 0
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
# Usage example
|
770
|
+
user_id = "user123"
|
771
|
+
doc_id = "doc456"
|
772
|
+
timestamp = Familia.now.to_i
|
773
|
+
|
774
|
+
# Grant read + write permissions
|
775
|
+
permissions = Document::READ | Document::WRITE # 3
|
776
|
+
score = UserDocumentAccess.encode_score(timestamp, permissions)
|
777
|
+
|
778
|
+
# Store in sorted set (user_id -> score with permissions)
|
779
|
+
user_documents = Familia::DataType::SortedSet.new("user:#{user_id}:documents")
|
780
|
+
user_documents.add(doc_id, score)
|
781
|
+
|
782
|
+
# Query with permission filtering
|
783
|
+
docs_with_write = user_documents.select do |doc_id, score|
|
784
|
+
_, permissions = UserDocumentAccess.decode_score(score)
|
785
|
+
UserDocumentAccess.has_permission?(permissions, Document::WRITE)
|
786
|
+
end
|
787
|
+
```
|
788
|
+
|
789
|
+
### Time-Series Relationships with Automatic Management
|
790
|
+
Track relationships over time with timestamp-based scoring and automatic updates.
|
791
|
+
|
792
|
+
```ruby
|
793
|
+
class ActivityTracker < Familia::Horreum
|
794
|
+
feature :relationships
|
795
|
+
|
796
|
+
identifier_field :activity_id
|
797
|
+
field :activity_id, :user_id, :activity_type, :data, :created_at
|
798
|
+
|
799
|
+
# Class-level tracking with automatic management
|
800
|
+
class_participates_in :user_activities, score: :created_at
|
801
|
+
class_participates_in :activity_by_type,
|
802
|
+
score: ->(activity) { "#{activity.activity_type}:#{activity.created_at}".hash }
|
803
|
+
end
|
804
|
+
|
805
|
+
# Create and save activity (automatic tracking)
|
806
|
+
activity = ActivityTracker.new(
|
807
|
+
activity_id: 'act123',
|
808
|
+
user_id: 'user456',
|
809
|
+
activity_type: 'login',
|
810
|
+
created_at: Familia.now.to_i
|
811
|
+
)
|
812
|
+
activity.save # Automatically added to both tracking collections
|
813
|
+
|
814
|
+
# Query recent activities (last hour)
|
815
|
+
hour_ago = (Familia.now - 1.hour).to_i
|
816
|
+
recent_activities = ActivityTracker.user_activities.range_by_score(
|
817
|
+
hour_ago, '+inf', limit: [0, 50]
|
818
|
+
)
|
819
|
+
|
820
|
+
# Get activities by type in time range
|
821
|
+
login_activities = ActivityTracker.activity_by_type.range_by_score(
|
822
|
+
"login:#{hour_ago}".hash, "login:#{Familia.now.to_i}".hash
|
823
|
+
)
|
824
|
+
```
|
825
|
+
|
826
|
+
---
|
827
|
+
|
828
|
+
## Data Type Usage Patterns
|
829
|
+
|
830
|
+
### Advanced Sorted UnsortedSet Operations
|
831
|
+
Leverage Valkey/Redis sorted sets for rankings, time series, and scored data.
|
832
|
+
|
833
|
+
```ruby
|
834
|
+
class Leaderboard < Familia::Horreum
|
835
|
+
identifier_field :game_id
|
836
|
+
field :game_id, :name
|
837
|
+
sorted_set :scores
|
838
|
+
end
|
839
|
+
|
840
|
+
leaderboard = Leaderboard.new(game_id: "game1", name: "Daily Challenge")
|
841
|
+
|
842
|
+
# Add player scores
|
843
|
+
leaderboard.scores.add("player1", 1500)
|
844
|
+
leaderboard.scores.add("player2", 2300)
|
845
|
+
leaderboard.scores.add("player3", 1800)
|
846
|
+
|
847
|
+
# Get top 10 players
|
848
|
+
top_players = leaderboard.scores.range(0, 9, with_scores: true, order: 'DESC')
|
849
|
+
# => [["player2", 2300.0], ["player3", 1800.0], ["player1", 1500.0]]
|
850
|
+
|
851
|
+
# Get player rank
|
852
|
+
rank = leaderboard.scores.rank("player1", order: 'DESC') # => 2 (0-indexed)
|
853
|
+
|
854
|
+
# Get score range
|
855
|
+
mid_tier = leaderboard.scores.range_by_score(1000, 2000, with_scores: true)
|
856
|
+
|
857
|
+
# Increment score atomically
|
858
|
+
leaderboard.scores.increment("player1", 100) # Add 100 to existing score
|
859
|
+
```
|
860
|
+
|
861
|
+
### List-Based Queues and Feeds
|
862
|
+
Use Valkey/Redis lists for queues, feeds, and ordered data.
|
863
|
+
|
864
|
+
```ruby
|
865
|
+
class TaskQueue < Familia::Horreum
|
866
|
+
identifier_field :queue_name
|
867
|
+
field :queue_name
|
868
|
+
list :tasks
|
869
|
+
end
|
870
|
+
|
871
|
+
class ActivityFeed < Familia::Horreum
|
872
|
+
identifier_field :user_id
|
873
|
+
field :user_id
|
874
|
+
list :activities
|
875
|
+
end
|
876
|
+
|
877
|
+
# Task queue operations
|
878
|
+
queue = TaskQueue.new(queue_name: "email_processing")
|
879
|
+
|
880
|
+
# Add tasks to queue (right push)
|
881
|
+
queue.tasks.push({
|
882
|
+
type: "send_email",
|
883
|
+
recipient: "user@example.com",
|
884
|
+
template: "welcome"
|
885
|
+
}.to_json)
|
886
|
+
|
887
|
+
# Process tasks (left pop - FIFO)
|
888
|
+
next_task = queue.tasks.pop # Atomic pop from left
|
889
|
+
task_data = JSON.parse(next_task) if next_task
|
890
|
+
|
891
|
+
# Activity feed with size limit
|
892
|
+
feed = ActivityFeed.new(user_id: "user123")
|
893
|
+
|
894
|
+
# Add activity (keep last 100)
|
895
|
+
feed.activities.unshift("User logged in at #{Familia.now}")
|
896
|
+
feed.activities.trim(0, 99) # Keep only last 100 items
|
897
|
+
|
898
|
+
# Get recent activities
|
899
|
+
recent = feed.activities.range(0, 9) # Get 10 most recent
|
900
|
+
```
|
901
|
+
|
902
|
+
### Hash-Based Configuration Storage
|
903
|
+
Store structured configuration and key-value data.
|
904
|
+
|
905
|
+
```ruby
|
906
|
+
class UserPreferences < Familia::Horreum
|
907
|
+
identifier_field :user_id
|
908
|
+
field :user_id
|
909
|
+
hash :settings
|
910
|
+
hash :feature_flags
|
911
|
+
end
|
912
|
+
|
913
|
+
prefs = UserPreferences.new(user_id: "user123")
|
914
|
+
|
915
|
+
# UnsortedSet individual preferences
|
916
|
+
prefs.settings.set("theme", "dark")
|
917
|
+
prefs.settings.set("notifications", "true")
|
918
|
+
prefs.settings.set("timezone", "UTC-5")
|
919
|
+
|
920
|
+
# Batch set multiple values
|
921
|
+
prefs.feature_flags.update({
|
922
|
+
"beta_ui" => "true",
|
923
|
+
"new_dashboard" => "false",
|
924
|
+
"advanced_features" => "true"
|
925
|
+
})
|
926
|
+
|
927
|
+
# Get preferences
|
928
|
+
theme = prefs.settings.get("theme") # => "dark"
|
929
|
+
all_settings = prefs.settings.all # => Hash of all settings
|
930
|
+
|
931
|
+
# Check feature flags
|
932
|
+
beta_enabled = prefs.feature_flags.get("beta_ui") == "true"
|
933
|
+
```
|
934
|
+
|
935
|
+
---
|
936
|
+
|
937
|
+
## Error Handling and Validation
|
938
|
+
|
939
|
+
### Connection Error Handling
|
940
|
+
Robust error handling for Valkey/Redis connection issues.
|
941
|
+
|
942
|
+
```ruby
|
943
|
+
class ResilientService < Familia::Horreum
|
944
|
+
field :name, :data
|
945
|
+
|
946
|
+
def self.with_fallback(&block)
|
947
|
+
retries = 3
|
948
|
+
begin
|
949
|
+
yield
|
950
|
+
rescue Redis::ConnectionError, Redis::TimeoutError => e
|
951
|
+
retries -= 1
|
952
|
+
if retries > 0
|
953
|
+
sleep(0.1 * (4 - retries)) # Exponential backoff
|
954
|
+
retry
|
955
|
+
else
|
956
|
+
Familia.warn "Redis operation failed after retries: #{e.message}"
|
957
|
+
nil # Return nil or handle gracefully
|
958
|
+
end
|
959
|
+
end
|
960
|
+
end
|
961
|
+
|
962
|
+
def save_with_fallback
|
963
|
+
self.class.with_fallback { save }
|
964
|
+
end
|
965
|
+
end
|
966
|
+
```
|
967
|
+
|
968
|
+
### Data Validation Patterns
|
969
|
+
Implement validation in model classes.
|
970
|
+
|
971
|
+
```ruby
|
972
|
+
class User < Familia::Horreum
|
973
|
+
field :email, :username, :age
|
974
|
+
|
975
|
+
def valid?
|
976
|
+
errors.clear
|
977
|
+
validate_email
|
978
|
+
validate_username
|
979
|
+
validate_age
|
980
|
+
errors.empty?
|
981
|
+
end
|
982
|
+
|
983
|
+
def errors
|
984
|
+
@errors ||= []
|
985
|
+
end
|
986
|
+
|
987
|
+
private
|
988
|
+
|
989
|
+
def validate_email
|
990
|
+
unless email&.include?('@')
|
991
|
+
errors << "Email must be valid"
|
992
|
+
end
|
993
|
+
end
|
994
|
+
|
995
|
+
def validate_username
|
996
|
+
if username.nil? || username.length < 3
|
997
|
+
errors << "Username must be at least 3 characters"
|
998
|
+
end
|
999
|
+
end
|
1000
|
+
|
1001
|
+
def validate_age
|
1002
|
+
unless age.is_a?(Integer) && age > 0
|
1003
|
+
errors << "Age must be a positive integer"
|
1004
|
+
end
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
# Usage
|
1009
|
+
user = User.new(email: "invalid", username: "ab", age: -5)
|
1010
|
+
if user.valid?
|
1011
|
+
user.save
|
1012
|
+
else
|
1013
|
+
puts "Validation errors: #{user.errors.join(', ')}"
|
1014
|
+
end
|
1015
|
+
```
|
1016
|
+
|
1017
|
+
---
|
1018
|
+
|
1019
|
+
## Performance Optimization
|
1020
|
+
|
1021
|
+
### Batch Operations
|
1022
|
+
Minimize Valkey/Redis round trips with batch operations.
|
1023
|
+
|
1024
|
+
```ruby
|
1025
|
+
# Instead of multiple individual operations
|
1026
|
+
users = []
|
1027
|
+
100.times do |i|
|
1028
|
+
user = User.new(name: "User #{i}", email: "user#{i}@example.com")
|
1029
|
+
users << user
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
# Use Valkey/Redis pipelining for batch saves
|
1033
|
+
User.transaction do |redis|
|
1034
|
+
users.each do |user|
|
1035
|
+
# All operations batched in transaction
|
1036
|
+
user.object.set_all(user.to_hash)
|
1037
|
+
User.email_index.set(user.email, user.identifier)
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
```
|
1041
|
+
|
1042
|
+
### Memory Optimization
|
1043
|
+
Efficient memory usage patterns.
|
1044
|
+
|
1045
|
+
```ruby
|
1046
|
+
class CacheEntry < Familia::Horreum
|
1047
|
+
feature :expiration
|
1048
|
+
|
1049
|
+
field :key, :value, :created_at
|
1050
|
+
default_expiration 1.hour
|
1051
|
+
|
1052
|
+
# Use shorter field names to reduce memory
|
1053
|
+
field :k, :v, :c # Instead of key, value, created_at
|
1054
|
+
|
1055
|
+
# Compress large values
|
1056
|
+
def value=(val)
|
1057
|
+
@value = val.length > 1000 ? Zlib.deflate(val) : val
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
def value
|
1061
|
+
val = @value || ""
|
1062
|
+
val.start_with?("\x78\x9c") ? Zlib.inflate(val) : val
|
1063
|
+
rescue Zlib::DataError
|
1064
|
+
@value # Return original if decompression fails
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
```
|
1068
|
+
|
1069
|
+
### Connection Pool Sizing
|
1070
|
+
Configure connection pools based on application needs.
|
1071
|
+
|
1072
|
+
```ruby
|
1073
|
+
# High-throughput application
|
1074
|
+
Familia.connection_provider = lambda do |uri|
|
1075
|
+
ConnectionPool.new(size: 25, timeout: 5) do
|
1076
|
+
Redis.new(uri, connect_timeout: 0.1, read_timeout: 1, write_timeout: 1)
|
1077
|
+
end.with { |conn| yield conn if block_given?; conn }
|
1078
|
+
end
|
1079
|
+
|
1080
|
+
# Memory-constrained environment
|
1081
|
+
Familia.connection_provider = lambda do |uri|
|
1082
|
+
ConnectionPool.new(size: 5, timeout: 10) do
|
1083
|
+
Redis.new(uri, connect_timeout: 2, read_timeout: 5, write_timeout: 5)
|
1084
|
+
end.with { |conn| yield conn if block_given?; conn }
|
1085
|
+
end
|
1086
|
+
```
|
1087
|
+
|
1088
|
+
---
|
1089
|
+
|
1090
|
+
## Migration and Upgrading
|
1091
|
+
|
1092
|
+
### From v1.x to v2.0.0-pre
|
1093
|
+
Key changes and migration steps.
|
1094
|
+
|
1095
|
+
```ruby
|
1096
|
+
# OLD v1.x syntax
|
1097
|
+
class User < Familia
|
1098
|
+
identifier :email
|
1099
|
+
string :name
|
1100
|
+
list :sessions
|
1101
|
+
end
|
1102
|
+
|
1103
|
+
# NEW v2.0.0-pre syntax
|
1104
|
+
class User < Familia::Horreum
|
1105
|
+
identifier_field :email # Updated method name
|
1106
|
+
field :name # Generic field method
|
1107
|
+
list :sessions # Data types unchanged
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
# Feature activation (NEW)
|
1111
|
+
class User < Familia::Horreum
|
1112
|
+
feature :expiration # Explicit feature activation
|
1113
|
+
feature :safe_dump
|
1114
|
+
|
1115
|
+
identifier_field :email
|
1116
|
+
field :name
|
1117
|
+
list :sessions
|
1118
|
+
|
1119
|
+
default_expiration 24.hours # Feature-specific methods
|
1120
|
+
safe_dump_field :name # Feature-specific methods
|
1121
|
+
end
|
1122
|
+
```
|
1123
|
+
|
1124
|
+
### Encryption Migration
|
1125
|
+
Migrating existing fields to encrypted storage.
|
1126
|
+
|
1127
|
+
```ruby
|
1128
|
+
# Step 1: Add feature without changing existing fields
|
1129
|
+
class User < Familia::Horreum
|
1130
|
+
feature :encrypted_fields # Add feature
|
1131
|
+
|
1132
|
+
field :name, :email
|
1133
|
+
field :api_key # Still plaintext during migration
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
# Step 2: Migrate data with dual read/write
|
1137
|
+
class User < Familia::Horreum
|
1138
|
+
feature :encrypted_fields
|
1139
|
+
|
1140
|
+
field :name, :email
|
1141
|
+
encrypted_field :api_key # Now encrypted
|
1142
|
+
|
1143
|
+
# Temporary migration method
|
1144
|
+
def migrate_api_key!
|
1145
|
+
if raw_api_key = object.get("api_key") # Read old plaintext
|
1146
|
+
self.api_key = raw_api_key # Write as encrypted
|
1147
|
+
object.delete("api_key") # Remove plaintext
|
1148
|
+
save
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
|
1153
|
+
# Step 3: Run migration for all users
|
1154
|
+
User.all.each(&:migrate_api_key!)
|
1155
|
+
```
|
1156
|
+
|
1157
|
+
---
|
1158
|
+
|
1159
|
+
## Testing Patterns
|
1160
|
+
|
1161
|
+
### Test Helpers and Utilities
|
1162
|
+
Common patterns for testing Familia applications.
|
1163
|
+
|
1164
|
+
```ruby
|
1165
|
+
# test_helper.rb
|
1166
|
+
require 'familia'
|
1167
|
+
|
1168
|
+
# Use separate Valkey/Redis database for tests
|
1169
|
+
Familia.configure do |config|
|
1170
|
+
config.redis_uri = ENV.fetch('REDIS_TEST_URI', 'redis://localhost:6379/15')
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
module TestHelpers
|
1174
|
+
def setup_redis
|
1175
|
+
# Clear test database
|
1176
|
+
Familia.connection.flushdb
|
1177
|
+
end
|
1178
|
+
|
1179
|
+
def teardown_redis
|
1180
|
+
Familia.connection.flushdb
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
def create_test_user(**attrs)
|
1184
|
+
User.new({
|
1185
|
+
email: "test@example.com",
|
1186
|
+
name: "Test User",
|
1187
|
+
created_at: Familia.now.to_i
|
1188
|
+
}.merge(attrs))
|
1189
|
+
end
|
1190
|
+
end
|
1191
|
+
|
1192
|
+
# In test files
|
1193
|
+
class UserTest < Minitest::Test
|
1194
|
+
include TestHelpers
|
1195
|
+
|
1196
|
+
def setup
|
1197
|
+
setup_redis
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
def teardown
|
1201
|
+
teardown_redis
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
def test_user_creation_with_automatic_indexing
|
1205
|
+
user = create_test_user(name: "Alice")
|
1206
|
+
user.save # Automatically adds to class-level indexes
|
1207
|
+
|
1208
|
+
assert user.exists?
|
1209
|
+
assert_equal "Alice", user.name
|
1210
|
+
|
1211
|
+
# Test automatic indexing
|
1212
|
+
found_id = User.email_lookup.get(user.email)
|
1213
|
+
assert_equal user.identifier, found_id
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
def test_relationships_with_clean_syntax
|
1217
|
+
user = create_test_user
|
1218
|
+
user.save # Automatic class-level tracking
|
1219
|
+
|
1220
|
+
domain = Domain.new(domain_id: "test_domain", name: "test.com")
|
1221
|
+
domain.save # Automatic class-level tracking
|
1222
|
+
|
1223
|
+
# Test clean relationship syntax
|
1224
|
+
user.domains << domain # Ruby-like collection syntax
|
1225
|
+
assert domain.in_user_domains?(user.identifier)
|
1226
|
+
assert user.domains.member?(domain.identifier)
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
def test_encrypted_fields_concealment
|
1230
|
+
setup_encryption_keys
|
1231
|
+
|
1232
|
+
vault = SecureVault.new(
|
1233
|
+
name: "Test Vault",
|
1234
|
+
secret_key: "super-secret-123",
|
1235
|
+
api_token: "sk-1234567890"
|
1236
|
+
)
|
1237
|
+
vault.save
|
1238
|
+
|
1239
|
+
# Test ConcealedString behavior
|
1240
|
+
assert_instance_of Familia::Features::EncryptedFields::ConcealedString, vault.secret_key
|
1241
|
+
assert_equal "[CONCEALED]", vault.secret_key.to_s
|
1242
|
+
assert_equal "super-secret-123", vault.secret_key.reveal
|
1243
|
+
|
1244
|
+
# Test JSON safety
|
1245
|
+
json_data = vault.to_json
|
1246
|
+
refute_includes json_data, "super-secret-123"
|
1247
|
+
assert_includes json_data, "[CONCEALED]"
|
1248
|
+
end
|
1249
|
+
|
1250
|
+
def test_transient_fields_non_persistence
|
1251
|
+
form = LoginForm.new(
|
1252
|
+
username: "testuser",
|
1253
|
+
password: "secret123",
|
1254
|
+
csrf_token: "abc123"
|
1255
|
+
)
|
1256
|
+
form.save
|
1257
|
+
|
1258
|
+
# Reload and verify transient fields not persisted
|
1259
|
+
reloaded = LoginForm.load(form.identifier)
|
1260
|
+
assert_equal "testuser", reloaded.username
|
1261
|
+
assert_nil reloaded.password
|
1262
|
+
assert_nil reloaded.csrf_token
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
def test_object_identifier_generation
|
1266
|
+
# Test UUID generation
|
1267
|
+
doc = UuidDocument.create(title: "Test Doc")
|
1268
|
+
assert_match(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i, doc.objid)
|
1269
|
+
|
1270
|
+
# Test hex generation
|
1271
|
+
session = HexSession.create(user_id: "123")
|
1272
|
+
assert_match(/\A[0-9a-f]+\z/i, session.objid)
|
1273
|
+
assert_equal 16, session.objid.length
|
1274
|
+
end
|
1275
|
+
|
1276
|
+
def test_quantization_time_bucketing
|
1277
|
+
timestamp = Time.parse("2024-12-15 14:37:23")
|
1278
|
+
bucket = MetricsBucket.quantize_timestamp(timestamp)
|
1279
|
+
assert_equal "20241215_1430", bucket # 10-minute bucket
|
1280
|
+
|
1281
|
+
# Test multiple timestamps in same bucket
|
1282
|
+
timestamp2 = Time.parse("2024-12-15 14:39:45")
|
1283
|
+
bucket2 = MetricsBucket.quantize_timestamp(timestamp2)
|
1284
|
+
assert_equal bucket, bucket2
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
def test_external_identifier_mapping
|
1288
|
+
user = ExternalUser.new(
|
1289
|
+
internal_id: SecureRandom.uuid,
|
1290
|
+
external_id: "ext_123456",
|
1291
|
+
name: "External User"
|
1292
|
+
)
|
1293
|
+
user.save
|
1294
|
+
|
1295
|
+
# Test bidirectional mapping
|
1296
|
+
found_by_external = ExternalUser.find_by_external_id("ext_123456")
|
1297
|
+
assert_equal user.internal_id, found_by_external.internal_id
|
1298
|
+
|
1299
|
+
# Test sync status tracking
|
1300
|
+
user.mark_sync_pending
|
1301
|
+
assert_equal "pending", user.sync_status
|
1302
|
+
|
1303
|
+
user.mark_sync_completed
|
1304
|
+
assert_equal "completed", user.sync_status
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
private
|
1308
|
+
|
1309
|
+
def setup_encryption_keys
|
1310
|
+
test_keys = {
|
1311
|
+
v1: Base64.strict_encode64('a' * 32),
|
1312
|
+
v2: Base64.strict_encode64('b' * 32)
|
1313
|
+
}
|
1314
|
+
Familia.configure do |config|
|
1315
|
+
config.encryption_keys = test_keys
|
1316
|
+
config.current_key_version = :v1
|
1317
|
+
config.encryption_personalization = 'TestApp-Test'
|
1318
|
+
end
|
1319
|
+
end
|
1320
|
+
end
|
1321
|
+
```
|
1322
|
+
|
1323
|
+
---
|
1324
|
+
|
1325
|
+
## Resources and References
|
1326
|
+
|
1327
|
+
### Key Configuration
|
1328
|
+
Essential configuration options for Familia v2.0.0-pre.
|
1329
|
+
|
1330
|
+
```ruby
|
1331
|
+
Familia.configure do |config|
|
1332
|
+
# Basic Valkey/Redis connection
|
1333
|
+
config.redis_uri = ENV['REDIS_URL'] || 'redis://localhost:6379/0'
|
1334
|
+
|
1335
|
+
# Connection provider for pooling (optional)
|
1336
|
+
config.connection_provider = MyConnectionProvider
|
1337
|
+
|
1338
|
+
# Encryption configuration (for encrypted_fields feature)
|
1339
|
+
config.encryption_keys = {
|
1340
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY_V1'],
|
1341
|
+
v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
|
1342
|
+
}
|
1343
|
+
config.current_key_version = :v2
|
1344
|
+
|
1345
|
+
# Debugging and logging
|
1346
|
+
config.debug = ENV['FAMILIA_DEBUG'] == 'true'
|
1347
|
+
config.enable_database_logging = ENV['FAMILIA_LOG_REDIS'] == 'true'
|
1348
|
+
end
|
1349
|
+
```
|
1350
|
+
|
1351
|
+
### Documentation Links
|
1352
|
+
- [Familia Repository](https://github.com/delano/familia)
|
1353
|
+
- [Wiki Home](../guides/Home.md)
|
1354
|
+
- [Feature System Guide](../guides/Feature-System-Guide.md)
|
1355
|
+
- [Relationships Guide](../guides/Relationships-Guide.md)
|
1356
|
+
- [Encrypted Fields Overview](../guides/Encrypted-Fields-Overview.md)
|
1357
|
+
- [Connection Pooling Guide](../guides/Connection-Pooling-Guide.md)
|
1358
|
+
|
1359
|
+
### Version Information
|
1360
|
+
- **Current Version**: v2.0.0.pre6 (as of version.rb)
|
1361
|
+
- **Target Version**: v2.0.0.pre7 (relationships release)
|
1362
|
+
- **Ruby Compatibility**: 3.0+ (3.4+ recommended for optimal threading)
|
1363
|
+
- **Redis Compatibility**: 6.0+ (Valkey compatible)
|
1364
|
+
|
1365
|
+
This technical reference covers the major components and usage patterns available in Familia v2.0.0-pre series. For complete API documentation, see the generated YARD docs and wiki guides.
|