familia 2.0.0.pre5 → 2.0.0.pre7
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/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,684 @@
|
|
1
|
+
# Relationships Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
The Relationships feature provides a sophisticated system for managing object relationships in Familia applications. It enables objects to track membership, create bidirectional associations, and maintain indexed lookups while supporting advanced features like permission bit encoding and time-based analytics.
|
6
|
+
|
7
|
+
## Core Concepts
|
8
|
+
|
9
|
+
### Relationship Types
|
10
|
+
|
11
|
+
The Familia v2.0 relationships system provides three distinct relationship patterns:
|
12
|
+
|
13
|
+
1. **`tracked_in`** - Multi-presence tracking with score encoding (sorted sets)
|
14
|
+
2. **`indexed_by`** - O(1) hash-based lookups by field values
|
15
|
+
3. **`member_of`** - Bidirectional membership with collision-free naming
|
16
|
+
|
17
|
+
Each type is optimized for different use cases and provides specific performance characteristics.
|
18
|
+
|
19
|
+
## Basic Usage
|
20
|
+
|
21
|
+
### Enabling Relationships
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Customer < Familia::Horreum
|
25
|
+
feature :relationships # Enable relationship functionality
|
26
|
+
|
27
|
+
identifier_field :custid
|
28
|
+
field :custid, :name, :email
|
29
|
+
|
30
|
+
# Define relationship collections
|
31
|
+
tracked_in :active_users, type: :sorted_set
|
32
|
+
indexed_by :email_lookup, type: :hash
|
33
|
+
end
|
34
|
+
|
35
|
+
class Domain < Familia::Horreum
|
36
|
+
feature :relationships
|
37
|
+
|
38
|
+
identifier_field :domain_id
|
39
|
+
field :domain_id, :name, :dns_zone
|
40
|
+
|
41
|
+
# Define bidirectional membership
|
42
|
+
member_of Customer, :domains, type: :set
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
## Tracked In Relationships
|
47
|
+
|
48
|
+
### Basic Tracking
|
49
|
+
|
50
|
+
The `tracked_in` relationship creates collections that track object membership with sophisticated scoring:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class User < Familia::Horreum
|
54
|
+
feature :relationships
|
55
|
+
|
56
|
+
identifier_field :user_id
|
57
|
+
field :user_id, :name, :score_value
|
58
|
+
|
59
|
+
# Simple sorted set tracking
|
60
|
+
tracked_in :leaderboard, type: :sorted_set, score: :score_value
|
61
|
+
|
62
|
+
# Time-based tracking with automatic timestamps
|
63
|
+
tracked_in :activity_log, type: :sorted_set
|
64
|
+
|
65
|
+
# Proc-based scoring for complex calculations
|
66
|
+
tracked_in :performance_metrics, type: :sorted_set,
|
67
|
+
score: ->(user) { (user.score_value || 0) * 2 }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Usage
|
71
|
+
user = User.new(user_id: 'user123', score_value: 85)
|
72
|
+
|
73
|
+
# Add to collections
|
74
|
+
User.add_to_leaderboard(user) # Uses score_value (85)
|
75
|
+
User.add_to_activity_log(user) # Uses current timestamp
|
76
|
+
User.add_to_performance_metrics(user) # Uses proc result (170)
|
77
|
+
|
78
|
+
# Query collections
|
79
|
+
User.leaderboard.score('user123') # => 85.0
|
80
|
+
User.activity_log.range_by_score('-inf', '+inf') # All users by time
|
81
|
+
User.performance_metrics.rank('user123') # User's rank by performance
|
82
|
+
```
|
83
|
+
|
84
|
+
### Score Encoding System
|
85
|
+
|
86
|
+
The relationships feature includes a sophisticated bit encoding system for permissions and metadata:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class Document < Familia::Horreum
|
90
|
+
feature :relationships
|
91
|
+
|
92
|
+
identifier_field :doc_id
|
93
|
+
field :doc_id, :title, :content
|
94
|
+
|
95
|
+
# Permission-based tracking with 8-bit encoding
|
96
|
+
tracked_in :authorized_users, type: :sorted_set,
|
97
|
+
score: :encode_permissions
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def encode_permissions
|
102
|
+
# Combine timestamp with permission bits
|
103
|
+
timestamp = Time.now.to_f.floor
|
104
|
+
permissions = calculate_user_permissions # Returns 0-255
|
105
|
+
"#{timestamp}.#{permissions}".to_f
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
#### Permission Bit Flags
|
111
|
+
|
112
|
+
The system supports 8 permission flags (0-255 range):
|
113
|
+
|
114
|
+
| Flag | Value | Permission | Description |
|
115
|
+
|------|-------|------------|-------------|
|
116
|
+
| read | 1 | Read access | View document content |
|
117
|
+
| append | 2 | Append access | Add new content |
|
118
|
+
| write | 4 | Write access | Modify existing content |
|
119
|
+
| edit | 8 | Edit access | Full content editing |
|
120
|
+
| configure | 16 | Configure access | Change document settings |
|
121
|
+
| delete | 32 | Delete access | Remove document |
|
122
|
+
| transfer | 64 | Transfer access | Change ownership |
|
123
|
+
| admin | 128 | Admin access | Full administrative control |
|
124
|
+
|
125
|
+
#### Predefined Permission Roles
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# Permission combinations for common roles
|
129
|
+
ROLES = {
|
130
|
+
viewer: 1, # read only
|
131
|
+
editor: 1 | 2 | 4, # read + append + write
|
132
|
+
moderator: 15, # read + append + write + edit
|
133
|
+
admin: 255 # all permissions
|
134
|
+
}
|
135
|
+
|
136
|
+
# Usage with score encoding
|
137
|
+
class DocumentAccess
|
138
|
+
include Familia::Features::Relationships::ScoreEncoding
|
139
|
+
|
140
|
+
def grant_access(user_id, role = :viewer)
|
141
|
+
permissions = ROLES[role]
|
142
|
+
encoded_score = encode_score_with_permissions(permissions)
|
143
|
+
Document.authorized_users.add(user_id, encoded_score)
|
144
|
+
end
|
145
|
+
|
146
|
+
def check_permission(user_id, permission_flag)
|
147
|
+
score = Document.authorized_users.score(user_id)
|
148
|
+
return false unless score
|
149
|
+
|
150
|
+
_, permissions = decode_score_with_permissions(score)
|
151
|
+
(permissions & permission_flag) != 0
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Example usage
|
156
|
+
access = DocumentAccess.new
|
157
|
+
access.grant_access('user123', :editor)
|
158
|
+
access.check_permission('user123', 4) # => true (write permission)
|
159
|
+
access.check_permission('user123', 32) # => false (no delete permission)
|
160
|
+
```
|
161
|
+
|
162
|
+
#### Time-Based Queries with Permissions
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
# Range queries combining time and permissions
|
166
|
+
class DocumentAnalytics
|
167
|
+
def users_with_access_since(timestamp, min_permissions = 1)
|
168
|
+
min_score = "#{timestamp}.#{min_permissions}".to_f
|
169
|
+
Document.authorized_users.range_by_score(min_score, '+inf')
|
170
|
+
end
|
171
|
+
|
172
|
+
def admin_users_last_week
|
173
|
+
week_ago = (Time.now - 7.days).to_f.floor
|
174
|
+
admin_permissions = 128
|
175
|
+
min_score = "#{week_ago}.#{admin_permissions}".to_f
|
176
|
+
|
177
|
+
Document.authorized_users.range_by_score(min_score, '+inf')
|
178
|
+
end
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
## Indexed By Relationships
|
183
|
+
|
184
|
+
### Hash-Based Lookups
|
185
|
+
|
186
|
+
The `indexed_by` relationship creates O(1) hash-based indexes for field values:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class User < Familia::Horreum
|
190
|
+
feature :relationships
|
191
|
+
|
192
|
+
field :email, :username, :department
|
193
|
+
|
194
|
+
# Create indexes for fast lookups
|
195
|
+
indexed_by :email_index, field: :email
|
196
|
+
indexed_by :username_index, field: :username
|
197
|
+
indexed_by :department_index, field: :department
|
198
|
+
end
|
199
|
+
|
200
|
+
# Usage
|
201
|
+
user = User.new(email: 'john@example.com', username: 'johndoe')
|
202
|
+
User.add_to_email_index(user)
|
203
|
+
User.add_to_username_index(user)
|
204
|
+
|
205
|
+
# Fast O(1) lookups
|
206
|
+
user_id = User.email_index.get('john@example.com') # => user.identifier
|
207
|
+
user_id = User.username_index.get('johndoe') # => user.identifier
|
208
|
+
|
209
|
+
# Batch operations
|
210
|
+
users = [user1, user2, user3]
|
211
|
+
users.each { |u| User.add_to_email_index(u) }
|
212
|
+
|
213
|
+
# Check if indexed
|
214
|
+
User.email_index.exists?('john@example.com') # => true
|
215
|
+
```
|
216
|
+
|
217
|
+
### Multi-Field Indexing
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
class Product < Familia::Horreum
|
221
|
+
feature :relationships
|
222
|
+
|
223
|
+
field :category, :brand, :status
|
224
|
+
|
225
|
+
# Composite indexing for complex queries
|
226
|
+
indexed_by :category_brand_index, field: ->(product) {
|
227
|
+
"#{product.category}:#{product.brand}"
|
228
|
+
}
|
229
|
+
|
230
|
+
indexed_by :active_products, field: ->(product) {
|
231
|
+
product.status == 'active' ? product.identifier : nil
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
# Usage
|
236
|
+
product = Product.new(category: 'electronics', brand: 'apple', status: 'active')
|
237
|
+
Product.add_to_category_brand_index(product)
|
238
|
+
Product.add_to_active_products(product)
|
239
|
+
|
240
|
+
# Query by composite key
|
241
|
+
Product.category_brand_index.get('electronics:apple') # => product.identifier
|
242
|
+
Product.active_products.exists?(product.identifier) # => true
|
243
|
+
```
|
244
|
+
|
245
|
+
## Member Of Relationships
|
246
|
+
|
247
|
+
### Bidirectional Membership
|
248
|
+
|
249
|
+
The `member_of` relationship creates bidirectional associations with collision-free method naming:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
class Customer < Familia::Horreum
|
253
|
+
feature :relationships
|
254
|
+
|
255
|
+
identifier_field :custid
|
256
|
+
field :custid, :name
|
257
|
+
|
258
|
+
# Collections for owned objects
|
259
|
+
set :domains
|
260
|
+
list :projects
|
261
|
+
set :users
|
262
|
+
end
|
263
|
+
|
264
|
+
class Domain < Familia::Horreum
|
265
|
+
feature :relationships
|
266
|
+
|
267
|
+
identifier_field :domain_id
|
268
|
+
field :domain_id, :name
|
269
|
+
|
270
|
+
# Declare membership in customer collections
|
271
|
+
member_of Customer, :domains, type: :set
|
272
|
+
end
|
273
|
+
|
274
|
+
class Project < Familia::Horreum
|
275
|
+
feature :relationships
|
276
|
+
|
277
|
+
identifier_field :project_id
|
278
|
+
field :project_id, :name
|
279
|
+
|
280
|
+
member_of Customer, :projects, type: :list
|
281
|
+
end
|
282
|
+
|
283
|
+
class User < Familia::Horreum
|
284
|
+
feature :relationships
|
285
|
+
|
286
|
+
identifier_field :user_id
|
287
|
+
field :user_id, :email
|
288
|
+
|
289
|
+
member_of Customer, :users, type: :set
|
290
|
+
end
|
291
|
+
```
|
292
|
+
|
293
|
+
### Collision-Free Method Generation
|
294
|
+
|
295
|
+
The system automatically generates collision-free methods when multiple classes have the same collection name:
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
class Team < Familia::Horreum
|
299
|
+
feature :relationships
|
300
|
+
set :users # Same collection name as Customer
|
301
|
+
end
|
302
|
+
|
303
|
+
class User < Familia::Horreum
|
304
|
+
feature :relationships
|
305
|
+
|
306
|
+
# Both memberships create unique methods
|
307
|
+
member_of Customer, :users, type: :set
|
308
|
+
member_of Team, :users, type: :set
|
309
|
+
end
|
310
|
+
|
311
|
+
# Generated methods are collision-free:
|
312
|
+
user = User.new(user_id: 'user123')
|
313
|
+
|
314
|
+
# Add to different collections
|
315
|
+
user.add_to_customer_users(customer.custid) # Specific to Customer.users
|
316
|
+
user.add_to_team_users(team.team_id) # Specific to Team.users
|
317
|
+
|
318
|
+
# Check membership
|
319
|
+
user.in_customer_users?(customer.custid) # => true
|
320
|
+
user.in_team_users?(team.team_id) # => true
|
321
|
+
|
322
|
+
# Remove from specific collections
|
323
|
+
user.remove_from_customer_users(customer.custid)
|
324
|
+
user.remove_from_team_users(team.team_id)
|
325
|
+
```
|
326
|
+
|
327
|
+
### Multi-Context Membership Patterns
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
class Document < Familia::Horreum
|
331
|
+
feature :relationships
|
332
|
+
|
333
|
+
identifier_field :doc_id
|
334
|
+
field :doc_id, :title
|
335
|
+
|
336
|
+
# Multiple membership contexts
|
337
|
+
member_of Customer, :documents, type: :set
|
338
|
+
member_of Project, :documents, type: :list
|
339
|
+
member_of Team, :shared_docs, type: :sorted_set
|
340
|
+
end
|
341
|
+
|
342
|
+
# Usage - same document can belong to multiple contexts
|
343
|
+
doc = Document.new(doc_id: 'doc123', title: 'Requirements')
|
344
|
+
|
345
|
+
# Add to different organizational contexts
|
346
|
+
doc.add_to_customer_documents(customer.custid)
|
347
|
+
doc.add_to_project_documents(project.project_id)
|
348
|
+
doc.add_to_team_shared_docs(team.team_id, score: Time.now.to_i)
|
349
|
+
|
350
|
+
# Query membership across contexts
|
351
|
+
doc.in_customer_documents?(customer.custid) # => true
|
352
|
+
doc.in_project_documents?(project.project_id) # => true
|
353
|
+
doc.in_team_shared_docs?(team.team_id) # => true
|
354
|
+
```
|
355
|
+
|
356
|
+
## Advanced Features
|
357
|
+
|
358
|
+
### Atomic Multi-Collection Operations
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
class BusinessLogic
|
362
|
+
def transfer_domain(domain, from_customer, to_customer)
|
363
|
+
# Atomic transfer across multiple collections
|
364
|
+
Familia.transaction do |conn|
|
365
|
+
# Remove from old customer
|
366
|
+
domain.remove_from_customer_domains(from_customer.custid)
|
367
|
+
from_customer.domains.remove(domain.identifier)
|
368
|
+
|
369
|
+
# Add to new customer
|
370
|
+
domain.add_to_customer_domains(to_customer.custid)
|
371
|
+
to_customer.domains.add(domain.identifier)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def bulk_permission_update(user_ids, new_permissions)
|
376
|
+
Document.authorized_users.pipeline do |pipe|
|
377
|
+
user_ids.each do |user_id|
|
378
|
+
current_score = Document.authorized_users.score(user_id)
|
379
|
+
if current_score
|
380
|
+
timestamp = current_score.floor
|
381
|
+
new_score = "#{timestamp}.#{new_permissions}".to_f
|
382
|
+
pipe.zadd(Document.authorized_users.key, new_score, user_id)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
```
|
389
|
+
|
390
|
+
### Performance Optimizations
|
391
|
+
|
392
|
+
```ruby
|
393
|
+
class OptimizedQueries
|
394
|
+
# Batch membership checks
|
395
|
+
def check_multiple_memberships(user_ids, customer)
|
396
|
+
# Single Redis call instead of multiple
|
397
|
+
Customer.users.pipeline do |pipe|
|
398
|
+
user_ids.each { |uid| pipe.sismember(customer.users.key, uid) }
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Efficient range queries with permissions
|
403
|
+
def recent_editors_with_write_access(hours = 24)
|
404
|
+
since = (Time.now - hours.hours).to_f.floor
|
405
|
+
write_permission = 4
|
406
|
+
min_score = "#{since}.#{write_permission}".to_f
|
407
|
+
|
408
|
+
Document.authorized_users.range_by_score(min_score, '+inf')
|
409
|
+
end
|
410
|
+
|
411
|
+
# Batch index updates
|
412
|
+
def reindex_users(users)
|
413
|
+
User.email_index.pipeline do |pipe|
|
414
|
+
users.each do |user|
|
415
|
+
pipe.hset(User.email_index.key, user.email, user.identifier)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
```
|
421
|
+
|
422
|
+
## Integration Patterns
|
423
|
+
|
424
|
+
### Multi-Tenant Applications
|
425
|
+
|
426
|
+
```ruby
|
427
|
+
class Organization < Familia::Horreum
|
428
|
+
feature :relationships
|
429
|
+
|
430
|
+
identifier_field :org_id
|
431
|
+
field :org_id, :name, :plan
|
432
|
+
|
433
|
+
# Organization collections
|
434
|
+
set :members
|
435
|
+
set :projects
|
436
|
+
sorted_set :activity_feed
|
437
|
+
end
|
438
|
+
|
439
|
+
class User < Familia::Horreum
|
440
|
+
feature :relationships
|
441
|
+
|
442
|
+
identifier_field :user_id
|
443
|
+
field :user_id, :email, :role
|
444
|
+
|
445
|
+
# Multi-tenant membership
|
446
|
+
member_of Organization, :members, type: :set
|
447
|
+
tracked_in :global_activity, type: :sorted_set
|
448
|
+
indexed_by :email_lookup, field: :email
|
449
|
+
end
|
450
|
+
|
451
|
+
class Project < Familia::Horreum
|
452
|
+
feature :relationships
|
453
|
+
|
454
|
+
identifier_field :project_id
|
455
|
+
field :project_id, :name, :status
|
456
|
+
|
457
|
+
member_of Organization, :projects, type: :set
|
458
|
+
tracked_in :status_timeline, type: :sorted_set,
|
459
|
+
score: ->(proj) { "#{Time.now.to_i}.#{proj.status.hash}" }
|
460
|
+
end
|
461
|
+
|
462
|
+
# Usage
|
463
|
+
org = Organization.new(org_id: 'org123', name: 'Acme Corp')
|
464
|
+
user = User.new(user_id: 'user456', email: 'john@acme.com')
|
465
|
+
project = Project.new(project_id: 'proj789', name: 'Website')
|
466
|
+
|
467
|
+
# Establish relationships
|
468
|
+
user.add_to_organization_members(org.org_id)
|
469
|
+
project.add_to_organization_projects(org.org_id)
|
470
|
+
|
471
|
+
# Query organization structure
|
472
|
+
org.members.size # Number of organization members
|
473
|
+
org.projects.members # All project IDs in organization
|
474
|
+
|
475
|
+
# Global indexes
|
476
|
+
User.add_to_email_lookup(user)
|
477
|
+
user_id = User.email_lookup.get('john@acme.com') # Fast email lookup
|
478
|
+
```
|
479
|
+
|
480
|
+
### Analytics and Reporting
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
class AnalyticsService
|
484
|
+
def user_engagement_report(days = 30)
|
485
|
+
since = (Time.now - days.days).to_f.floor
|
486
|
+
|
487
|
+
# Get all users active in time period
|
488
|
+
active_users = User.global_activity.range_by_score(since, '+inf')
|
489
|
+
|
490
|
+
# Analyze permission levels
|
491
|
+
permission_breakdown = Document.authorized_users
|
492
|
+
.range_by_score(since, '+inf', with_scores: true)
|
493
|
+
.group_by { |user_id, score| decode_permissions(score) }
|
494
|
+
|
495
|
+
{
|
496
|
+
total_active_users: active_users.size,
|
497
|
+
permission_breakdown: permission_breakdown.transform_values(&:size),
|
498
|
+
top_contributors: User.global_activity.range(0, 9, with_scores: true)
|
499
|
+
}
|
500
|
+
end
|
501
|
+
|
502
|
+
def project_status_timeline(project_id)
|
503
|
+
project = Project.find(project_id)
|
504
|
+
Project.status_timeline
|
505
|
+
.range_by_score('-inf', '+inf', with_scores: true)
|
506
|
+
.select { |id, _| id == project_id }
|
507
|
+
.map { |_, score| decode_status_change(score) }
|
508
|
+
end
|
509
|
+
|
510
|
+
private
|
511
|
+
|
512
|
+
def decode_permissions(score)
|
513
|
+
_, permissions = score.to_s.split('.').map(&:to_i)
|
514
|
+
case permissions
|
515
|
+
when 1 then :viewer
|
516
|
+
when 15 then :moderator
|
517
|
+
when 255 then :admin
|
518
|
+
else :custom
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def decode_status_change(score)
|
523
|
+
timestamp, status_hash = score.to_s.split('.').map(&:to_i)
|
524
|
+
{
|
525
|
+
timestamp: Time.at(timestamp),
|
526
|
+
status: reverse_status_hash(status_hash)
|
527
|
+
}
|
528
|
+
end
|
529
|
+
end
|
530
|
+
```
|
531
|
+
|
532
|
+
## Testing Relationships
|
533
|
+
|
534
|
+
### RSpec Testing Patterns
|
535
|
+
|
536
|
+
```ruby
|
537
|
+
RSpec.describe "Relationships Feature" do
|
538
|
+
let(:customer) { Customer.new(custid: 'cust123', name: 'Acme Corp') }
|
539
|
+
let(:domain) { Domain.new(domain_id: 'dom456', name: 'acme.com') }
|
540
|
+
let(:user) { User.new(user_id: 'user789', email: 'john@acme.com') }
|
541
|
+
|
542
|
+
describe "tracked_in relationships" do
|
543
|
+
it "tracks objects with score encoding" do
|
544
|
+
User.add_to_leaderboard(user)
|
545
|
+
score = User.leaderboard.score(user.identifier)
|
546
|
+
|
547
|
+
expect(score).to be_a(Float)
|
548
|
+
expect(User.leaderboard.rank(user.identifier)).to be >= 0
|
549
|
+
end
|
550
|
+
|
551
|
+
it "supports permission bit encoding" do
|
552
|
+
# Test permission encoding
|
553
|
+
encoded = encode_score_with_permissions(15) # moderator permissions
|
554
|
+
timestamp, permissions = decode_score_with_permissions(encoded)
|
555
|
+
|
556
|
+
expect(permissions).to eq(15)
|
557
|
+
expect(timestamp).to be_within(1).of(Time.now.to_i)
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
describe "indexed_by relationships" do
|
562
|
+
it "creates O(1) hash lookups" do
|
563
|
+
User.add_to_email_lookup(user)
|
564
|
+
found_id = User.email_lookup.get(user.email)
|
565
|
+
|
566
|
+
expect(found_id).to eq(user.identifier)
|
567
|
+
end
|
568
|
+
|
569
|
+
it "handles batch operations" do
|
570
|
+
users = [user, user2, user3]
|
571
|
+
users.each { |u| User.add_to_email_lookup(u) }
|
572
|
+
|
573
|
+
users.each do |u|
|
574
|
+
expect(User.email_lookup.get(u.email)).to eq(u.identifier)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
describe "member_of relationships" do
|
580
|
+
it "creates bidirectional associations" do
|
581
|
+
domain.add_to_customer_domains(customer.custid)
|
582
|
+
customer.domains.add(domain.identifier)
|
583
|
+
|
584
|
+
expect(domain.in_customer_domains?(customer.custid)).to be true
|
585
|
+
expect(customer.domains.member?(domain.identifier)).to be true
|
586
|
+
end
|
587
|
+
|
588
|
+
it "generates collision-free methods" do
|
589
|
+
expect(domain).to respond_to(:add_to_customer_domains)
|
590
|
+
expect(domain).to respond_to(:in_customer_domains?)
|
591
|
+
expect(domain).to respond_to(:remove_from_customer_domains)
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
```
|
596
|
+
|
597
|
+
### Integration Testing
|
598
|
+
|
599
|
+
```ruby
|
600
|
+
RSpec.describe "Relationships Integration" do
|
601
|
+
scenario "multi-tenant organization with full relationship graph" do
|
602
|
+
# Create organization structure
|
603
|
+
org = Organization.create(org_id: 'org123', name: 'Tech Corp')
|
604
|
+
|
605
|
+
# Create users with different roles
|
606
|
+
admin = User.create(user_id: 'admin1', email: 'admin@tech.com', role: 'admin')
|
607
|
+
dev = User.create(user_id: 'dev1', email: 'dev@tech.com', role: 'developer')
|
608
|
+
|
609
|
+
# Create projects
|
610
|
+
project = Project.create(project_id: 'proj1', name: 'Web App')
|
611
|
+
|
612
|
+
# Establish all relationships
|
613
|
+
admin.add_to_organization_members(org.org_id)
|
614
|
+
dev.add_to_organization_members(org.org_id)
|
615
|
+
project.add_to_organization_projects(org.org_id)
|
616
|
+
|
617
|
+
# Add to indexes
|
618
|
+
User.add_to_email_lookup(admin)
|
619
|
+
User.add_to_email_lookup(dev)
|
620
|
+
|
621
|
+
# Test relationship integrity
|
622
|
+
expect(org.members.size).to eq(2)
|
623
|
+
expect(org.projects.size).to eq(1)
|
624
|
+
|
625
|
+
# Test lookups
|
626
|
+
expect(User.email_lookup.get('admin@tech.com')).to eq(admin.identifier)
|
627
|
+
expect(User.email_lookup.get('dev@tech.com')).to eq(dev.identifier)
|
628
|
+
|
629
|
+
# Test membership queries
|
630
|
+
expect(admin.in_organization_members?(org.org_id)).to be true
|
631
|
+
expect(project.in_organization_projects?(org.org_id)).to be true
|
632
|
+
end
|
633
|
+
end
|
634
|
+
```
|
635
|
+
|
636
|
+
## Best Practices
|
637
|
+
|
638
|
+
### Relationship Design
|
639
|
+
|
640
|
+
1. **Choose the Right Type**:
|
641
|
+
- Use `tracked_in` for activity feeds, leaderboards, time-series data
|
642
|
+
- Use `indexed_by` for fast lookups by field values
|
643
|
+
- Use `member_of` for bidirectional ownership/membership
|
644
|
+
|
645
|
+
2. **Score Encoding Strategy**:
|
646
|
+
- Combine timestamps with metadata for rich queries
|
647
|
+
- Use bit flags for permissions (supports 8 flags efficiently)
|
648
|
+
- Consider sort order requirements when designing scores
|
649
|
+
|
650
|
+
3. **Performance Optimization**:
|
651
|
+
- Batch operations when possible using pipelines
|
652
|
+
- Use appropriate Redis data types for your access patterns
|
653
|
+
- Index only frequently-queried fields
|
654
|
+
|
655
|
+
### Memory and Storage
|
656
|
+
|
657
|
+
1. **Efficient Bit Encoding**:
|
658
|
+
- 8 bits can encode 256 permission combinations
|
659
|
+
- Single Redis sorted set score contains time + permissions
|
660
|
+
- Reduces memory vs. separate permission records
|
661
|
+
|
662
|
+
2. **Key Design**:
|
663
|
+
- Relationship keys follow pattern: `class:field:collection`
|
664
|
+
- Collision-free method names prevent namespace conflicts
|
665
|
+
- Predictable key structure aids debugging
|
666
|
+
|
667
|
+
3. **Cleanup Strategies**:
|
668
|
+
- Remove objects from all relationship collections on deletion
|
669
|
+
- Use TTL on temporary relationship data
|
670
|
+
- Regular cleanup of stale indexes
|
671
|
+
|
672
|
+
### Security Considerations
|
673
|
+
|
674
|
+
1. **Permission Validation**:
|
675
|
+
- Always validate permissions before operations
|
676
|
+
- Use bit flags for efficient permission checking
|
677
|
+
- Audit permission changes with timestamps
|
678
|
+
|
679
|
+
2. **Access Control**:
|
680
|
+
- Verify relationship membership before granting access
|
681
|
+
- Use consistent permission models across features
|
682
|
+
- Log relationship changes for audit trails
|
683
|
+
|
684
|
+
The Relationships feature provides a comprehensive foundation for building sophisticated multi-tenant applications with efficient object relationships, permission management, and analytics capabilities.
|