familia 2.0.0.pre15 → 2.0.0.pre16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +64 -4
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +3 -5
- data/lib/familia/features.rb +4 -13
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +75 -43
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -246,7 +246,7 @@ end
|
|
246
246
|
```ruby
|
247
247
|
session = UserSession.find('session_123')
|
248
248
|
|
249
|
-
# Check TTL using Redis TTL command (returns seconds remaining)
|
249
|
+
# Check TTL using Valkey/Redis TTL command (returns seconds remaining)
|
250
250
|
ttl_seconds = session.ttl # e.g., 1800 (30 minutes left)
|
251
251
|
|
252
252
|
# Convert to more readable format
|
@@ -303,7 +303,7 @@ class DataRetentionService
|
|
303
303
|
|
304
304
|
def self.apply_retention_policies
|
305
305
|
TTL_POLICIES.each do |data_type, ttl|
|
306
|
-
model_class = data_type.to_s.
|
306
|
+
model_class = data_type.to_s.pascalize.constantize
|
307
307
|
|
308
308
|
model_class.all.each do |record|
|
309
309
|
record.update_expiration(default_expiration: ttl)
|
@@ -593,4 +593,13 @@ Familia.configure do |config|
|
|
593
593
|
end
|
594
594
|
```
|
595
595
|
|
596
|
+
---
|
597
|
+
|
598
|
+
## See Also
|
599
|
+
|
600
|
+
- **[Technical Reference](../reference/api-technical.md#expiration-feature-v200-pre5)** - Implementation details and advanced patterns
|
601
|
+
- **[Overview](../overview.md#automatic-expiration)** - Conceptual introduction to expiration
|
602
|
+
- **[Feature System Guide](feature-system.md)** - Understanding Familia's feature architecture
|
603
|
+
- **[Implementation Guide](implementation.md)** - Production deployment and configuration patterns
|
604
|
+
|
596
605
|
The Expiration feature provides a robust foundation for managing data lifecycle in Familia applications, with flexible configuration options and automatic cascading to related objects.
|
@@ -0,0 +1,637 @@
|
|
1
|
+
# External Identifiers Guide
|
2
|
+
|
3
|
+
> **💡 Quick Reference**
|
4
|
+
>
|
5
|
+
> Enable integration with external systems and legacy databases:
|
6
|
+
> ```ruby
|
7
|
+
> class ExternalUser < Familia::Horreum
|
8
|
+
> feature :external_identifier
|
9
|
+
> field :internal_id, :external_id, :name, :sync_status
|
10
|
+
> end
|
11
|
+
> ```
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
The External Identifier feature provides seamless integration between Familia objects and external systems. Whether you're migrating from a legacy database, integrating with third-party APIs, or maintaining bidirectional synchronization with external services, this feature handles identifier mapping, validation, and sync status tracking.
|
16
|
+
|
17
|
+
## Why Use External Identifiers?
|
18
|
+
|
19
|
+
**Legacy Integration**: Migrate existing systems while maintaining references to original identifiers.
|
20
|
+
|
21
|
+
**API Synchronization**: Keep local objects synchronized with external services using their native identifiers.
|
22
|
+
|
23
|
+
**Dual-Key Strategy**: Maintain both internal Familia identifiers and external system identifiers for robust integration.
|
24
|
+
|
25
|
+
**Sync Tracking**: Built-in status tracking for synchronization operations and failure handling.
|
26
|
+
|
27
|
+
**Validation**: Ensure external identifiers meet format requirements and business rules.
|
28
|
+
|
29
|
+
## Quick Start
|
30
|
+
|
31
|
+
### Basic External ID Mapping
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Customer < Familia::Horreum
|
35
|
+
feature :external_identifier
|
36
|
+
|
37
|
+
identifier_field :internal_id
|
38
|
+
field :internal_id, :external_id, :name, :email, :sync_status
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create with external mapping
|
42
|
+
customer = Customer.new(
|
43
|
+
internal_id: SecureRandom.uuid,
|
44
|
+
external_id: "ext_customer_12345",
|
45
|
+
name: "Acme Corporation",
|
46
|
+
email: "contact@acme.com"
|
47
|
+
)
|
48
|
+
customer.save # Automatically creates bidirectional mapping
|
49
|
+
|
50
|
+
# Find by external ID
|
51
|
+
found_customer = Customer.find_by_external_id("ext_customer_12345")
|
52
|
+
puts found_customer.name # => "Acme Corporation"
|
53
|
+
```
|
54
|
+
|
55
|
+
### Legacy Database Migration
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class LegacyAccount < Familia::Horreum
|
59
|
+
feature :external_identifier, prefix: "legacy"
|
60
|
+
|
61
|
+
identifier_field :familia_id
|
62
|
+
field :familia_id, :legacy_account_id, :username, :migration_status
|
63
|
+
|
64
|
+
# External ID validation
|
65
|
+
def valid_external_id?
|
66
|
+
legacy_account_id.present? &&
|
67
|
+
legacy_account_id.match?(/^LAC[A-Z]{2}\d{8}$/)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Migrate legacy data
|
72
|
+
legacy_user = LegacyAccount.new(
|
73
|
+
familia_id: SecureRandom.uuid,
|
74
|
+
legacy_account_id: "LACUS12345678",
|
75
|
+
username: "john_doe"
|
76
|
+
)
|
77
|
+
|
78
|
+
if legacy_user.valid_external_id?
|
79
|
+
legacy_user.save
|
80
|
+
legacy_user.mark_migration_completed
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
## Configuration Options
|
85
|
+
|
86
|
+
### Basic Configuration
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class ExternalResource < Familia::Horreum
|
90
|
+
feature :external_identifier,
|
91
|
+
validation_pattern: /^ext_\d{6,}$/,
|
92
|
+
source_system: "CustomerAPI",
|
93
|
+
bidirectional: true # Default
|
94
|
+
|
95
|
+
field :resource_id, :external_id, :data
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
**Configuration Parameters:**
|
100
|
+
- `validation_pattern`: Regex pattern for external ID validation
|
101
|
+
- `source_system`: Name of the external system (for logging/debugging)
|
102
|
+
- `bidirectional`: Enable bidirectional mapping (default: true)
|
103
|
+
- `prefix`: Optional prefix for mapping keys
|
104
|
+
|
105
|
+
### Advanced Validation
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class StrictExternalUser < Familia::Horreum
|
109
|
+
feature :external_identifier,
|
110
|
+
validation_pattern: /^user_[a-z0-9]{8,16}$/,
|
111
|
+
source_system: "AuthService"
|
112
|
+
|
113
|
+
field :user_id, :external_id, :username, :permissions
|
114
|
+
|
115
|
+
# Custom validation beyond pattern matching
|
116
|
+
def validate_external_id!
|
117
|
+
return false unless valid_external_id_format?
|
118
|
+
|
119
|
+
# Check against blacklist
|
120
|
+
blacklisted_ids = ["user_test", "user_admin", "user_system"]
|
121
|
+
return false if blacklisted_ids.include?(external_id)
|
122
|
+
|
123
|
+
# Verify with external service
|
124
|
+
external_service_response = AuthService.verify_user_id(external_id)
|
125
|
+
external_service_response['valid'] == true
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def valid_external_id_format?
|
131
|
+
external_id.present? && external_id.match?(self.class.validation_pattern)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
## Mapping and Lookup Operations
|
137
|
+
|
138
|
+
### Bidirectional Mapping
|
139
|
+
|
140
|
+
External identifiers automatically maintain bidirectional mappings for efficient lookups:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class Product < Familia::Horreum
|
144
|
+
feature :external_identifier
|
145
|
+
field :product_id, :external_sku, :name, :price
|
146
|
+
end
|
147
|
+
|
148
|
+
product = Product.create(
|
149
|
+
product_id: "familia_prod_123",
|
150
|
+
external_sku: "SKU-ABC-789",
|
151
|
+
name: "Widget Pro"
|
152
|
+
)
|
153
|
+
|
154
|
+
# Automatic bidirectional mapping is created:
|
155
|
+
# external_id_mapping["SKU-ABC-789"] = "familia_prod_123"
|
156
|
+
# internal_id_mapping["familia_prod_123"] = "SKU-ABC-789"
|
157
|
+
|
158
|
+
# Fast lookups in both directions
|
159
|
+
by_external = Product.find_by_external_id("SKU-ABC-789")
|
160
|
+
by_internal = Product.load("familia_prod_123")
|
161
|
+
|
162
|
+
# Both return the same object
|
163
|
+
by_external.product_id == by_internal.product_id # => true
|
164
|
+
```
|
165
|
+
|
166
|
+
### Batch Operations
|
167
|
+
|
168
|
+
Efficiently handle multiple external identifier operations:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
class BulkImporter
|
172
|
+
def self.import_external_users(external_data_array)
|
173
|
+
external_ids = external_data_array.map { |data| data['external_id'] }
|
174
|
+
|
175
|
+
# Batch lookup existing users
|
176
|
+
existing_users = ExternalUser.multiget_by_external_ids(external_ids)
|
177
|
+
existing_external_ids = existing_users.compact.map(&:external_id)
|
178
|
+
|
179
|
+
# Process only new users
|
180
|
+
new_data = external_data_array.reject do |data|
|
181
|
+
existing_external_ids.include?(data['external_id'])
|
182
|
+
end
|
183
|
+
|
184
|
+
# Batch create new users
|
185
|
+
new_users = new_data.map do |data|
|
186
|
+
ExternalUser.new(
|
187
|
+
internal_id: SecureRandom.uuid,
|
188
|
+
external_id: data['external_id'],
|
189
|
+
name: data['name'],
|
190
|
+
email: data['email']
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Batch save with transaction
|
195
|
+
ExternalUser.transaction do |redis|
|
196
|
+
new_users.each(&:save)
|
197
|
+
end
|
198
|
+
|
199
|
+
new_users
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
## Synchronization Status Tracking
|
205
|
+
|
206
|
+
### Built-in Sync Status Management
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
class SyncableResource < Familia::Horreum
|
210
|
+
feature :external_identifier
|
211
|
+
|
212
|
+
field :resource_id, :external_id, :data, :sync_status, :last_sync_at, :sync_error
|
213
|
+
|
214
|
+
def sync_to_external!
|
215
|
+
mark_sync_pending
|
216
|
+
|
217
|
+
begin
|
218
|
+
# Simulate external API call
|
219
|
+
response = ExternalAPI.update_resource(external_id, data: self.data)
|
220
|
+
|
221
|
+
if response.success?
|
222
|
+
mark_sync_completed
|
223
|
+
self.last_sync_at = Familia.now.to_i
|
224
|
+
save
|
225
|
+
else
|
226
|
+
mark_sync_failed(response.error_message)
|
227
|
+
end
|
228
|
+
rescue => e
|
229
|
+
mark_sync_failed(e.message)
|
230
|
+
raise
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def sync_from_external!
|
235
|
+
mark_sync_pending
|
236
|
+
|
237
|
+
begin
|
238
|
+
external_data = ExternalAPI.get_resource(external_id)
|
239
|
+
self.data = external_data['data']
|
240
|
+
mark_sync_completed
|
241
|
+
save
|
242
|
+
rescue => e
|
243
|
+
mark_sync_failed(e.message)
|
244
|
+
raise
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def needs_sync?
|
249
|
+
sync_status != 'completed' ||
|
250
|
+
(last_sync_at && (Familia.now.to_i - last_sync_at) > 1.hour)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Usage
|
255
|
+
resource = SyncableResource.find_by_external_id("ext_123")
|
256
|
+
|
257
|
+
if resource.needs_sync?
|
258
|
+
resource.sync_from_external!
|
259
|
+
end
|
260
|
+
|
261
|
+
puts resource.sync_status # => "completed", "pending", "failed"
|
262
|
+
```
|
263
|
+
|
264
|
+
### Sync Status Methods
|
265
|
+
|
266
|
+
The external identifier feature provides these built-in status methods:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
# Status management
|
270
|
+
object.mark_sync_pending
|
271
|
+
object.mark_sync_completed
|
272
|
+
object.mark_sync_failed(error_message)
|
273
|
+
|
274
|
+
# Status checking
|
275
|
+
object.sync_pending? # => true/false
|
276
|
+
object.sync_completed? # => true/false
|
277
|
+
object.sync_failed? # => true/false
|
278
|
+
|
279
|
+
# Error handling
|
280
|
+
object.sync_error # => error message if failed
|
281
|
+
object.clear_sync_error # Reset error state
|
282
|
+
```
|
283
|
+
|
284
|
+
## Integration Patterns
|
285
|
+
|
286
|
+
### API Integration with Webhooks
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
class WebhookHandler
|
290
|
+
def self.handle_external_update(webhook_data)
|
291
|
+
external_id = webhook_data['resource_id']
|
292
|
+
resource = ExternalResource.find_by_external_id(external_id)
|
293
|
+
|
294
|
+
if resource
|
295
|
+
# Update existing resource
|
296
|
+
resource.data = webhook_data['data']
|
297
|
+
resource.mark_sync_completed
|
298
|
+
resource.save
|
299
|
+
else
|
300
|
+
# Create new resource from webhook
|
301
|
+
resource = ExternalResource.create(
|
302
|
+
internal_id: SecureRandom.uuid,
|
303
|
+
external_id: external_id,
|
304
|
+
data: webhook_data['data']
|
305
|
+
)
|
306
|
+
resource.mark_sync_completed
|
307
|
+
end
|
308
|
+
|
309
|
+
resource
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Webhook endpoint
|
314
|
+
post '/webhook/external_updates' do
|
315
|
+
webhook_data = JSON.parse(request.body.read)
|
316
|
+
WebhookHandler.handle_external_update(webhook_data)
|
317
|
+
status 200
|
318
|
+
end
|
319
|
+
```
|
320
|
+
|
321
|
+
### Legacy Database Migration
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
class LegacyMigration
|
325
|
+
def self.migrate_customers_from_legacy_db
|
326
|
+
# Connect to legacy database
|
327
|
+
legacy_db = Sequel.connect(ENV['LEGACY_DATABASE_URL'])
|
328
|
+
|
329
|
+
legacy_db[:customers].each do |legacy_row|
|
330
|
+
# Check if already migrated
|
331
|
+
existing = Customer.find_by_external_id(legacy_row[:customer_id])
|
332
|
+
next if existing
|
333
|
+
|
334
|
+
# Create new Familia object
|
335
|
+
customer = Customer.new(
|
336
|
+
internal_id: SecureRandom.uuid,
|
337
|
+
external_id: legacy_row[:customer_id].to_s,
|
338
|
+
name: legacy_row[:company_name],
|
339
|
+
email: legacy_row[:email],
|
340
|
+
created_at: legacy_row[:created_at].to_i
|
341
|
+
)
|
342
|
+
|
343
|
+
if customer.valid_external_id?
|
344
|
+
customer.save
|
345
|
+
customer.mark_migration_completed
|
346
|
+
puts "Migrated customer: #{customer.external_id}"
|
347
|
+
else
|
348
|
+
puts "Invalid external ID: #{legacy_row[:customer_id]}"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
```
|
354
|
+
|
355
|
+
### Multi-System Integration
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
class MultiSystemResource < Familia::Horreum
|
359
|
+
feature :external_identifier
|
360
|
+
|
361
|
+
field :internal_id, :crm_id, :billing_id, :support_id, :name
|
362
|
+
|
363
|
+
# Multiple external system mappings
|
364
|
+
def crm_mapping
|
365
|
+
@crm_mapping ||= ExternalIdMapping.new(self, :crm_id, "CRM_System")
|
366
|
+
end
|
367
|
+
|
368
|
+
def billing_mapping
|
369
|
+
@billing_mapping ||= ExternalIdMapping.new(self, :billing_id, "Billing_System")
|
370
|
+
end
|
371
|
+
|
372
|
+
def support_mapping
|
373
|
+
@support_mapping ||= ExternalIdMapping.new(self, :support_id, "Support_System")
|
374
|
+
end
|
375
|
+
|
376
|
+
def sync_to_all_systems!
|
377
|
+
[crm_mapping, billing_mapping, support_mapping].each do |mapping|
|
378
|
+
mapping.sync_to_external!
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
class ExternalIdMapping
|
383
|
+
def initialize(resource, id_field, system_name)
|
384
|
+
@resource = resource
|
385
|
+
@id_field = id_field
|
386
|
+
@system_name = system_name
|
387
|
+
end
|
388
|
+
|
389
|
+
def sync_to_external!
|
390
|
+
external_id = @resource.send(@id_field)
|
391
|
+
return unless external_id
|
392
|
+
|
393
|
+
case @system_name
|
394
|
+
when "CRM_System"
|
395
|
+
CRMApi.update_contact(external_id, @resource.to_crm_format)
|
396
|
+
when "Billing_System"
|
397
|
+
BillingApi.update_customer(external_id, @resource.to_billing_format)
|
398
|
+
when "Support_System"
|
399
|
+
SupportApi.update_user(external_id, @resource.to_support_format)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
```
|
405
|
+
|
406
|
+
## Performance Considerations
|
407
|
+
|
408
|
+
### Efficient Batch Lookups
|
409
|
+
|
410
|
+
```ruby
|
411
|
+
# Instead of individual lookups
|
412
|
+
external_ids = ["ext_1", "ext_2", "ext_3"]
|
413
|
+
users = external_ids.map { |id| User.find_by_external_id(id) }
|
414
|
+
|
415
|
+
# Use batch operations
|
416
|
+
users = User.multiget_by_external_ids(external_ids)
|
417
|
+
```
|
418
|
+
|
419
|
+
### Caching Strategies
|
420
|
+
|
421
|
+
```ruby
|
422
|
+
class CachedExternalResource < Familia::Horreum
|
423
|
+
feature :external_identifier
|
424
|
+
|
425
|
+
# Cache external ID mappings
|
426
|
+
def self.find_by_external_id_cached(external_id)
|
427
|
+
cache_key = "external_id_mapping:#{external_id}"
|
428
|
+
|
429
|
+
cached_internal_id = Familia.redis.get(cache_key)
|
430
|
+
if cached_internal_id
|
431
|
+
return load(cached_internal_id)
|
432
|
+
end
|
433
|
+
|
434
|
+
# Fallback to database lookup
|
435
|
+
resource = find_by_external_id(external_id)
|
436
|
+
if resource
|
437
|
+
Familia.redis.setex(cache_key, 300, resource.identifier)
|
438
|
+
end
|
439
|
+
|
440
|
+
resource
|
441
|
+
end
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
### Index Optimization
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
class OptimizedExternalResource < Familia::Horreum
|
449
|
+
feature :external_identifier
|
450
|
+
|
451
|
+
# Use dedicated sorted sets for each status with timestamp scores
|
452
|
+
sorted_set :pending_sync_resources,
|
453
|
+
score: ->(obj) { obj.last_sync_at&.to_i || 0 }
|
454
|
+
sorted_set :completed_sync_resources,
|
455
|
+
score: ->(obj) { obj.last_sync_at&.to_i || 0 }
|
456
|
+
sorted_set :failed_sync_resources,
|
457
|
+
score: ->(obj) { obj.last_sync_at&.to_i || 0 }
|
458
|
+
|
459
|
+
def self.pending_sync_resources(limit: 100)
|
460
|
+
# Query resources that need syncing, ordered by oldest first
|
461
|
+
pending_sync_resources.range(0, limit - 1).map { |id| load(id) }.compact
|
462
|
+
end
|
463
|
+
|
464
|
+
def self.recently_synced(status:, limit: 100)
|
465
|
+
# Get recently synced resources by status, newest first
|
466
|
+
case status.to_s
|
467
|
+
when 'pending'
|
468
|
+
pending_sync_resources.revrange(0, limit - 1).map { |id| load(id) }.compact
|
469
|
+
when 'completed'
|
470
|
+
completed_sync_resources.revrange(0, limit - 1).map { |id| load(id) }.compact
|
471
|
+
when 'failed'
|
472
|
+
failed_sync_resources.revrange(0, limit - 1).map { |id| load(id) }.compact
|
473
|
+
else
|
474
|
+
[]
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
```
|
479
|
+
|
480
|
+
## Testing Strategies
|
481
|
+
|
482
|
+
### Test External ID Integration
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
# test/models/external_user_test.rb
|
486
|
+
require 'test_helper'
|
487
|
+
|
488
|
+
class ExternalUserTest < Minitest::Test
|
489
|
+
def test_bidirectional_mapping
|
490
|
+
user = ExternalUser.create(
|
491
|
+
internal_id: "test_123",
|
492
|
+
external_id: "ext_456",
|
493
|
+
name: "Test User"
|
494
|
+
)
|
495
|
+
|
496
|
+
# Test lookup by external ID
|
497
|
+
found_by_external = ExternalUser.find_by_external_id("ext_456")
|
498
|
+
assert_equal user.internal_id, found_by_external.internal_id
|
499
|
+
|
500
|
+
# Test lookup by internal ID
|
501
|
+
found_by_internal = ExternalUser.load("test_123")
|
502
|
+
assert_equal user.external_id, found_by_internal.external_id
|
503
|
+
end
|
504
|
+
|
505
|
+
def test_sync_status_tracking
|
506
|
+
user = ExternalUser.create(
|
507
|
+
internal_id: "test_123",
|
508
|
+
external_id: "ext_456",
|
509
|
+
name: "Test User"
|
510
|
+
)
|
511
|
+
|
512
|
+
# Test status transitions
|
513
|
+
user.mark_sync_pending
|
514
|
+
assert user.sync_pending?
|
515
|
+
refute user.sync_completed?
|
516
|
+
|
517
|
+
user.mark_sync_completed
|
518
|
+
assert user.sync_completed?
|
519
|
+
refute user.sync_pending?
|
520
|
+
|
521
|
+
user.mark_sync_failed("Network error")
|
522
|
+
assert user.sync_failed?
|
523
|
+
assert_equal "Network error", user.sync_error
|
524
|
+
end
|
525
|
+
|
526
|
+
def test_external_id_validation
|
527
|
+
user = StrictExternalUser.new(
|
528
|
+
user_id: "test_123",
|
529
|
+
external_id: "invalid_format"
|
530
|
+
)
|
531
|
+
|
532
|
+
refute user.valid_external_id_format?
|
533
|
+
|
534
|
+
user.external_id = "user_validformat123"
|
535
|
+
assert user.valid_external_id_format?
|
536
|
+
end
|
537
|
+
end
|
538
|
+
```
|
539
|
+
|
540
|
+
### Mock External Services
|
541
|
+
|
542
|
+
```ruby
|
543
|
+
# test/support/external_service_mock.rb
|
544
|
+
class ExternalServiceMock
|
545
|
+
def self.setup_mocks
|
546
|
+
# Mock successful API responses
|
547
|
+
stub_request(:get, /external-api\.com\/resource\/ext_\d+/)
|
548
|
+
.to_return(
|
549
|
+
status: 200,
|
550
|
+
body: { data: "mocked_data", updated_at: Time.now.iso8601 }.to_json
|
551
|
+
)
|
552
|
+
|
553
|
+
stub_request(:post, /external-api\.com\/resource/)
|
554
|
+
.to_return(
|
555
|
+
status: 201,
|
556
|
+
body: { id: "ext_#{rand(1000)}", status: "created" }.to_json
|
557
|
+
)
|
558
|
+
end
|
559
|
+
|
560
|
+
def self.setup_error_mocks
|
561
|
+
# Mock API errors for testing error handling
|
562
|
+
stub_request(:get, /external-api\.com\/resource\/ext_error/)
|
563
|
+
.to_return(status: 500, body: "Internal Server Error")
|
564
|
+
end
|
565
|
+
end
|
566
|
+
```
|
567
|
+
|
568
|
+
## Troubleshooting
|
569
|
+
|
570
|
+
### Common Issues
|
571
|
+
|
572
|
+
**External ID Not Found**
|
573
|
+
```ruby
|
574
|
+
# Debug external ID mappings
|
575
|
+
puts ExternalUser.external_id_mapping.hgetall
|
576
|
+
# Shows all external_id -> internal_id mappings
|
577
|
+
|
578
|
+
# Check reverse mapping
|
579
|
+
puts ExternalUser.internal_id_mapping.hgetall
|
580
|
+
# Shows all internal_id -> external_id mappings
|
581
|
+
```
|
582
|
+
|
583
|
+
**Sync Status Issues**
|
584
|
+
```ruby
|
585
|
+
# Check sync status for all objects of a type
|
586
|
+
ExternalUser.all.each do |user|
|
587
|
+
puts "#{user.external_id}: #{user.sync_status} (#{user.sync_error})"
|
588
|
+
end
|
589
|
+
|
590
|
+
# Reset failed sync statuses
|
591
|
+
ExternalUser.all.select(&:sync_failed?).each(&:clear_sync_error)
|
592
|
+
```
|
593
|
+
|
594
|
+
**Validation Failures**
|
595
|
+
```ruby
|
596
|
+
user = ExternalUser.new(external_id: "invalid")
|
597
|
+
|
598
|
+
unless user.valid_external_id?
|
599
|
+
puts "Validation failed for: #{user.external_id}"
|
600
|
+
puts "Expected pattern: #{ExternalUser.validation_pattern}"
|
601
|
+
end
|
602
|
+
```
|
603
|
+
|
604
|
+
### Performance Debugging
|
605
|
+
|
606
|
+
```ruby
|
607
|
+
# Monitor external ID lookup performance
|
608
|
+
def benchmark_external_lookups(external_ids)
|
609
|
+
require 'benchmark'
|
610
|
+
|
611
|
+
Benchmark.bm(20) do |x|
|
612
|
+
x.report("Individual lookups:") do
|
613
|
+
external_ids.each { |id| ExternalUser.find_by_external_id(id) }
|
614
|
+
end
|
615
|
+
|
616
|
+
x.report("Batch lookups:") do
|
617
|
+
ExternalUser.multiget_by_external_ids(external_ids)
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
# Check mapping Valkey/Redis key sizes
|
623
|
+
mapping_keys = Familia.redis.keys("*external_id_mapping*")
|
624
|
+
mapping_keys.each do |key|
|
625
|
+
size = Familia.redis.hlen(key)
|
626
|
+
puts "#{key}: #{size} mappings"
|
627
|
+
end
|
628
|
+
```
|
629
|
+
|
630
|
+
---
|
631
|
+
|
632
|
+
## See Also
|
633
|
+
|
634
|
+
- **[Technical Reference](../reference/api-technical.md#external-identifier-feature-v200-pre7)** - Implementation details and advanced patterns
|
635
|
+
- **[Object Identifiers Guide](feature-object-identifiers.md)** - Automatic ID generation strategies
|
636
|
+
- **[Feature System Guide](feature-system.md)** - Understanding the feature architecture
|
637
|
+
- **[Implementation Guide](implementation.md)** - Advanced configuration and migration patterns
|