familia 2.0.0.pre4 → 2.0.0.pre6

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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,563 @@
1
+ # RelatableObjects Guide
2
+
3
+ ## Overview
4
+
5
+ The RelatableObjects feature provides a standardized system for managing object relationships and ownership in Familia applications. It enables objects to have unique identifiers, external references, and ownership relationships while maintaining API versioning and secure object management.
6
+
7
+ ## Core Concepts
8
+
9
+ ### Object Identity System
10
+
11
+ RelatableObjects introduces a dual-identifier system:
12
+
13
+ 1. **Object ID (`objid`)**: Internal UUID v7 for system use
14
+ 2. **External ID (`extid`)**: External-facing identifier for API consumers
15
+
16
+ ### Ownership Model
17
+
18
+ Objects can own other objects through a centralized ownership registry:
19
+ - **Owners**: Objects that possess other objects
20
+ - **Owned Objects**: Objects that belong to other objects
21
+ - **Ownership Validation**: Prevents self-ownership and enforces type checking
22
+
23
+ ## Basic Usage
24
+
25
+ ### Enabling RelatableObjects
26
+
27
+ ```ruby
28
+ class Customer < Familia::Horreum
29
+ feature :relatable_object # Note: uses relatable_object, not relatable_objects
30
+
31
+ field :name, :email, :plan
32
+ end
33
+
34
+ class Domain < Familia::Horreum
35
+ feature :relatable_object
36
+
37
+ field :name, :dns_zone
38
+ end
39
+ ```
40
+
41
+ ### Automatic ID Generation
42
+
43
+ ```ruby
44
+ customer = Customer.new(name: "Acme Corp", email: "admin@acme.com")
45
+
46
+ # IDs are lazily generated on first access
47
+ customer.objid # => "018c3f8e-7b2a-7f4a-9d8e-1a2b3c4d5e6f" (UUID v7)
48
+ customer.extid # => "ext_3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z" (54 chars)
49
+
50
+ # Alternative accessor methods
51
+ customer.relatable_objid # Same as objid
52
+ customer.external_identifier # Same as extid
53
+
54
+ # API version tracking
55
+ customer.api_version # => "v2" (automatically set)
56
+ ```
57
+
58
+ ### Object Relationships
59
+
60
+ ```ruby
61
+ # Establish ownership
62
+ customer = Customer.find_by_objid("018c3f8e-7b2a-7f4a-9d8e-1a2b3c4d5e6f")
63
+ domain = Domain.new(name: "acme.com")
64
+
65
+ # Set ownership (must be implemented by your application)
66
+ Customer.owners.set(domain.objid, customer.objid)
67
+
68
+ # Check ownership
69
+ domain.owner?(customer) # => true
70
+ domain.owned? # => true
71
+ customer.owner?(domain) # => false (customers don't have owners in this example)
72
+
73
+ # Find owner
74
+ owner_objid = Customer.owners.get(domain.objid)
75
+ owner = Customer.find_by_objid(owner_objid)
76
+ ```
77
+
78
+ ## Advanced Features
79
+
80
+ ### Custom Identifier Generation
81
+
82
+ ```ruby
83
+ # Override ID generation for custom formats
84
+ class CustomModel < Familia::Horreum
85
+ feature :relatable_object
86
+
87
+ def self.generate_objid
88
+ # Custom UUID generation
89
+ "custom_#{SecureRandom.uuid_v7}"
90
+ end
91
+
92
+ def self.generate_extid
93
+ # Custom external ID format
94
+ "ext_#{Time.now.to_i}_#{SecureRandom.hex(8)}"
95
+ end
96
+ end
97
+ ```
98
+
99
+ ### Ownership Management System
100
+
101
+ ```ruby
102
+ class OwnershipManager
103
+ # Grant ownership
104
+ def self.assign_owner(owned_object, owner_object)
105
+ # Validate objects are relatable
106
+ raise "Not relatable" unless owned_object.is_a?(V2::Features::RelatableObject)
107
+ raise "Not relatable" unless owner_object.is_a?(V2::Features::RelatableObject)
108
+
109
+ # Prevent self-ownership
110
+ raise "Self-ownership not allowed" if owned_object.class == owner_object.class
111
+
112
+ # Set ownership in registry
113
+ owner_object.class.owners.set(owned_object.objid, owner_object.objid)
114
+ end
115
+
116
+ # Remove ownership
117
+ def self.remove_owner(owned_object)
118
+ owned_object.class.owners.delete(owned_object.objid)
119
+ end
120
+
121
+ # Transfer ownership
122
+ def self.transfer_ownership(owned_object, new_owner)
123
+ remove_owner(owned_object)
124
+ assign_owner(owned_object, new_owner)
125
+ end
126
+
127
+ # Get owner
128
+ def self.get_owner(owned_object, owner_class)
129
+ owner_objid = owned_object.class.owners.get(owned_object.objid)
130
+ return nil if owner_objid.nil? || owner_objid.empty?
131
+
132
+ owner_class.find_by_objid(owner_objid)
133
+ end
134
+ end
135
+
136
+ # Usage
137
+ customer = Customer.new(name: "Acme Corp")
138
+ domain = Domain.new(name: "acme.com")
139
+
140
+ OwnershipManager.assign_owner(domain, customer)
141
+ OwnershipManager.get_owner(domain, Customer) # => customer object
142
+ ```
143
+
144
+ ### Multi-Tenant Patterns
145
+
146
+ ```ruby
147
+ class Organization < Familia::Horreum
148
+ feature :relatable_object
149
+
150
+ field :name, :plan, :domain
151
+
152
+ def users
153
+ User.owned_by(self)
154
+ end
155
+
156
+ def projects
157
+ Project.owned_by(self)
158
+ end
159
+ end
160
+
161
+ class User < Familia::Horreum
162
+ feature :relatable_object
163
+
164
+ field :email, :name, :role
165
+
166
+ def self.owned_by(organization)
167
+ owned_objids = owners.keys.select do |user_objid|
168
+ owners.get(user_objid) == organization.objid
169
+ end
170
+
171
+ owned_objids.map { |objid| find_by_objid(objid) }.compact
172
+ end
173
+
174
+ def organization
175
+ org_objid = self.class.owners.get(objid)
176
+ Organization.find_by_objid(org_objid)
177
+ end
178
+ end
179
+
180
+ class Project < Familia::Horreum
181
+ feature :relatable_object
182
+
183
+ field :name, :description, :status
184
+
185
+ def self.owned_by(organization)
186
+ # Similar implementation as User.owned_by
187
+ end
188
+ end
189
+
190
+ # Usage
191
+ org = Organization.create(name: "Acme Corp")
192
+ user = User.create(email: "john@acme.com", name: "John Doe")
193
+ project = Project.create(name: "Website Redesign")
194
+
195
+ OwnershipManager.assign_owner(user, org)
196
+ OwnershipManager.assign_owner(project, org)
197
+
198
+ org.users # => [user]
199
+ org.projects # => [project]
200
+ user.organization # => org
201
+ ```
202
+
203
+ ### API Integration Patterns
204
+
205
+ ```ruby
206
+ class APIController
207
+ # Use external IDs in API responses
208
+ def show_customer
209
+ customer = Customer.find_by_objid(params[:objid]) # Internal lookup
210
+
211
+ render json: {
212
+ id: customer.extid, # External ID for API consumers
213
+ name: customer.name,
214
+ email: customer.email,
215
+ api_version: customer.api_version,
216
+ domains: customer_domains(customer)
217
+ }
218
+ end
219
+
220
+ # Accept external IDs in API requests
221
+ def update_customer
222
+ # Convert external ID to internal lookup
223
+ extid = params[:id]
224
+ objid = resolve_external_id(extid, Customer)
225
+ customer = Customer.find_by_objid(objid)
226
+
227
+ customer.update(customer_params)
228
+
229
+ render json: { id: customer.extid, status: 'updated' }
230
+ end
231
+
232
+ private
233
+
234
+ def customer_domains(customer)
235
+ Domain.owned_by(customer).map do |domain|
236
+ {
237
+ id: domain.extid,
238
+ name: domain.name,
239
+ dns_zone: domain.dns_zone
240
+ }
241
+ end
242
+ end
243
+
244
+ def resolve_external_id(extid, klass)
245
+ # Implementation depends on your external ID tracking system
246
+ # This could be a separate mapping table or embedded in the extid format
247
+ ExternalIdMapping.objid_for(extid, klass)
248
+ end
249
+ end
250
+ ```
251
+
252
+ ## Object Lifecycle Management
253
+
254
+ ### Creation with Relationships
255
+
256
+ ```ruby
257
+ class CustomerCreationService
258
+ def self.create_with_domain(customer_attrs, domain_name)
259
+ customer = Customer.create(customer_attrs)
260
+ domain = Domain.create(name: domain_name)
261
+
262
+ # Establish ownership
263
+ OwnershipManager.assign_owner(domain, customer)
264
+
265
+ {
266
+ customer: customer,
267
+ domain: domain,
268
+ customer_id: customer.extid, # For API responses
269
+ domain_id: domain.extid
270
+ }
271
+ end
272
+ end
273
+
274
+ # Usage
275
+ result = CustomerCreationService.create_with_domain(
276
+ { name: "Acme Corp", email: "admin@acme.com" },
277
+ "acme.com"
278
+ )
279
+
280
+ puts result[:customer_id] # => "ext_3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z"
281
+ ```
282
+
283
+ ### Deletion with Cleanup
284
+
285
+ ```ruby
286
+ class CustomerDeletionService
287
+ def self.delete_with_owned_objects(customer)
288
+ # Find all owned objects
289
+ owned_domains = Domain.owned_by(customer)
290
+ owned_users = User.owned_by(customer)
291
+ owned_projects = Project.owned_by(customer)
292
+
293
+ # Delete owned objects first
294
+ owned_domains.each(&:delete)
295
+ owned_users.each(&:delete)
296
+ owned_projects.each(&:delete)
297
+
298
+ # Clean up ownership records
299
+ [Domain, User, Project].each do |klass|
300
+ cleanup_ownership_records(klass, customer)
301
+ end
302
+
303
+ # Finally delete the owner
304
+ customer.delete
305
+ end
306
+
307
+ private
308
+
309
+ def self.cleanup_ownership_records(owned_class, owner)
310
+ # Remove ownership records where this customer was the owner
311
+ owned_class.owners.keys.each do |owned_objid|
312
+ if owned_class.owners.get(owned_objid) == owner.objid
313
+ owned_class.owners.delete(owned_objid)
314
+ end
315
+ end
316
+ end
317
+ end
318
+ ```
319
+
320
+ ## Security Considerations
321
+
322
+ ### Access Control
323
+
324
+ ```ruby
325
+ class SecureAccessManager
326
+ def self.verify_ownership(user, resource)
327
+ return false unless user.is_a?(V2::Features::RelatableObject)
328
+ return false unless resource.is_a?(V2::Features::RelatableObject)
329
+
330
+ # Check direct ownership
331
+ return true if resource.owner?(user)
332
+
333
+ # Check organizational ownership
334
+ user_org = OwnershipManager.get_owner(user, Organization)
335
+ return true if user_org && resource.owner?(user_org)
336
+
337
+ false
338
+ end
339
+
340
+ def self.filter_owned_resources(user, resources)
341
+ resources.select { |resource| verify_ownership(user, resource) }
342
+ end
343
+ end
344
+
345
+ # In your controllers
346
+ class ResourceController
347
+ before_action :authenticate_user
348
+ before_action :load_resource, only: [:show, :update, :destroy]
349
+ before_action :verify_access, only: [:show, :update, :destroy]
350
+
351
+ private
352
+
353
+ def load_resource
354
+ @resource = Resource.find_by_objid(params[:objid])
355
+ not_found unless @resource
356
+ end
357
+
358
+ def verify_access
359
+ unless SecureAccessManager.verify_ownership(current_user, @resource)
360
+ unauthorized
361
+ end
362
+ end
363
+ end
364
+ ```
365
+
366
+ ### External ID Security
367
+
368
+ ```ruby
369
+ # Prevent external ID enumeration attacks
370
+ class SecureExternalIdGenerator
371
+ def self.generate_secure_extid(objid)
372
+ # Use HMAC to prevent guessing
373
+ timestamp = Time.now.to_i
374
+ data = "#{objid}:#{timestamp}"
375
+ hmac = OpenSSL::HMAC.hexdigest('SHA256', Rails.application.secret_key_base, data)
376
+
377
+ "ext_#{timestamp}_#{hmac[0..15]}" # 54 characters total
378
+ end
379
+
380
+ def self.validate_extid(extid, objid)
381
+ # Verify HMAC to prevent tampering
382
+ parts = extid.split('_')
383
+ return false unless parts.length == 3 && parts[0] == 'ext'
384
+
385
+ timestamp, provided_hmac = parts[1], parts[2]
386
+ expected_data = "#{objid}:#{timestamp}"
387
+ expected_hmac = OpenSSL::HMAC.hexdigest('SHA256', Rails.application.secret_key_base, expected_data)
388
+
389
+ expected_hmac[0..15] == provided_hmac
390
+ end
391
+ end
392
+
393
+ # Override in your models
394
+ class SecureCustomer < Familia::Horreum
395
+ feature :relatable_object
396
+
397
+ def self.generate_extid
398
+ # Will be called after objid is generated
399
+ SecureExternalIdGenerator.generate_secure_extid(objid)
400
+ end
401
+ end
402
+ ```
403
+
404
+ ## Performance Optimization
405
+
406
+ ### Batch Operations
407
+
408
+ ```ruby
409
+ class BatchOwnershipManager
410
+ def self.assign_multiple_owners(ownership_pairs)
411
+ # ownership_pairs: [{ owned: obj1, owner: obj2 }, ...]
412
+
413
+ # Group by owner class to minimize pipe operations
414
+ grouped = ownership_pairs.group_by { |pair| pair[:owner].class }
415
+
416
+ grouped.each do |owner_class, pairs|
417
+ owner_class.owners.pipeline do |pipe|
418
+ pairs.each do |pair|
419
+ pipe.hset(pair[:owned].objid, pair[:owner].objid)
420
+ end
421
+ end
422
+ end
423
+ end
424
+
425
+ def self.load_owners_for_objects(objects, owner_class)
426
+ return {} if objects.empty?
427
+
428
+ # Batch load all ownership records
429
+ objids = objects.map(&:objid)
430
+ owner_objids = owner_class.owners.mget(*objids)
431
+
432
+ # Batch load owner objects
433
+ valid_owner_objids = owner_objids.compact.uniq
434
+ owners = valid_owner_objids.map { |objid| owner_class.find_by_objid(objid) }.compact
435
+ owners_by_objid = owners.index_by(&:objid)
436
+
437
+ # Create mapping
438
+ objects.zip(owner_objids).to_h do |obj, owner_objid|
439
+ [obj, owner_objid ? owners_by_objid[owner_objid] : nil]
440
+ end
441
+ end
442
+ end
443
+
444
+ # Usage
445
+ domains = Domain.all
446
+ domain_owners = BatchOwnershipManager.load_owners_for_objects(domains, Customer)
447
+
448
+ domains.each do |domain|
449
+ owner = domain_owners[domain]
450
+ puts "#{domain.name} owned by #{owner&.name || 'nobody'}"
451
+ end
452
+ ```
453
+
454
+ ### Caching Strategies
455
+
456
+ ```ruby
457
+ class CachedOwnershipLookup
458
+ CACHE_TTL = 5.minutes
459
+
460
+ def self.get_owner(owned_object, owner_class)
461
+ cache_key = "owner:#{owned_object.objid}:#{owner_class.name}"
462
+
463
+ cached = Rails.cache.read(cache_key)
464
+ return cached if cached
465
+
466
+ owner = OwnershipManager.get_owner(owned_object, owner_class)
467
+ Rails.cache.write(cache_key, owner, expires_in: CACHE_TTL)
468
+
469
+ owner
470
+ end
471
+
472
+ def self.invalidate_ownership_cache(owned_object)
473
+ # Invalidate all possible owner class caches
474
+ [Customer, Organization, User].each do |owner_class|
475
+ cache_key = "owner:#{owned_object.objid}:#{owner_class.name}"
476
+ Rails.cache.delete(cache_key)
477
+ end
478
+ end
479
+ end
480
+ ```
481
+
482
+ ## Testing
483
+
484
+ ### RSpec Testing
485
+
486
+ ```ruby
487
+ RSpec.describe RelatableObject do
488
+ let(:customer) { Customer.create(name: "Test Customer") }
489
+ let(:domain) { Domain.create(name: "test.com") }
490
+
491
+ describe "ID generation" do
492
+ it "generates UUID v7 for objid" do
493
+ expect(customer.objid).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i)
494
+ end
495
+
496
+ it "generates 54-character external ID" do
497
+ expect(customer.extid).to start_with('ext_')
498
+ expect(customer.extid.length).to eq(54)
499
+ end
500
+ end
501
+
502
+ describe "ownership" do
503
+ before do
504
+ OwnershipManager.assign_owner(domain, customer)
505
+ end
506
+
507
+ it "establishes ownership relationship" do
508
+ expect(domain.owner?(customer)).to be true
509
+ expect(domain.owned?).to be true
510
+ end
511
+
512
+ it "prevents self-ownership" do
513
+ expect {
514
+ OwnershipManager.assign_owner(customer, customer)
515
+ }.to raise_error(/self-ownership/i)
516
+ end
517
+ end
518
+ end
519
+ ```
520
+
521
+ ### Integration Testing
522
+
523
+ ```ruby
524
+ RSpec.describe "RelatableObjects Integration" do
525
+ scenario "multi-tenant customer with domains and users" do
526
+ # Create organization
527
+ org = Organization.create(name: "Acme Corp")
528
+
529
+ # Create users and domains
530
+ user1 = User.create(email: "john@acme.com")
531
+ user2 = User.create(email: "jane@acme.com")
532
+ domain = Domain.create(name: "acme.com")
533
+
534
+ # Establish ownership
535
+ OwnershipManager.assign_owner(user1, org)
536
+ OwnershipManager.assign_owner(user2, org)
537
+ OwnershipManager.assign_owner(domain, org)
538
+
539
+ # Verify relationships
540
+ expect(org.users).to contain_exactly(user1, user2)
541
+ expect(org.domains).to contain_exactly(domain)
542
+ expect(user1.organization).to eq(org)
543
+ expect(domain.owner?(org)).to be true
544
+
545
+ # Test access control
546
+ expect(SecureAccessManager.verify_ownership(user1, domain)).to be true
547
+ expect(SecureAccessManager.verify_ownership(user2, domain)).to be true
548
+ end
549
+ end
550
+ ```
551
+
552
+ ## Best Practices
553
+
554
+ 1. **Consistent ID Usage**: Always use external IDs in APIs, internal objids for system operations
555
+ 2. **Ownership Validation**: Validate ownership before allowing operations
556
+ 3. **Batch Operations**: Use batch loading for performance when dealing with multiple objects
557
+ 4. **Cache Appropriately**: Cache ownership lookups but invalidate on changes
558
+ 5. **Secure External IDs**: Use HMACs or similar to prevent ID enumeration attacks
559
+ 6. **Clean Deletion**: Always clean up ownership records when deleting objects
560
+ 7. **Type Safety**: Validate object types in ownership operations
561
+ 8. **API Versioning**: Use the api_version field to handle API evolution
562
+
563
+ The RelatableObjects feature provides a robust foundation for building multi-tenant applications with secure object relationships and clear ownership models.