familia 2.0.0.pre15 → 2.0.0.pre17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. 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.camelize.constantize
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