familia 2.0.0.pre7 → 2.0.0.pre10
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/ci.yml +13 -0
- data/.github/workflows/docs.yml +1 -1
- data/.gitignore +9 -9
- data/.rubocop.yml +19 -0
- data/.yardopts +22 -1
- data/CHANGELOG.md +184 -0
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +97 -2
- data/changelog.d/README.md +66 -0
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +29 -0
- data/docs/archive/.gitignore +2 -0
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
- data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
- data/docs/archive/FAMILIA_UPDATE.md +226 -0
- data/docs/archive/README.md +67 -0
- data/docs/guides/.gitignore +2 -0
- data/docs/{wiki → guides}/Feature-System-Guide.md +0 -15
- data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
- data/docs/guides/relationships-methods.md +266 -0
- data/examples/relationships_basic.rb +90 -157
- data/familia.gemspec +4 -4
- data/lib/familia/connection.rb +4 -21
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
- data/lib/familia/features/external_identifiers.rb +111 -0
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
- data/lib/familia/features/object_identifiers.rb +194 -0
- data/lib/familia/features/relationships/cascading.rb +0 -1
- data/lib/familia/features/relationships/indexing.rb +160 -176
- data/lib/familia/features/relationships/membership.rb +16 -22
- data/lib/familia/features/relationships/querying.rb +7 -12
- data/lib/familia/features/relationships/score_encoding.rb +1 -3
- data/lib/familia/features/relationships/tracking.rb +61 -22
- data/lib/familia/features/relationships.rb +15 -8
- data/lib/familia/features/transient_fields.rb +8 -10
- data/lib/familia/features.rb +16 -13
- data/lib/familia/horreum/core/serialization.rb +2 -5
- data/lib/familia/horreum/subclass/definition.rb +36 -0
- data/lib/familia/horreum.rb +15 -24
- data/lib/familia/version.rb +1 -3
- data/setup.cfg +12 -0
- data/try/core/errors_try.rb +1 -1
- data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
- data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
- data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
- data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
- data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
- data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
- data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
- data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
- data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
- data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +339 -0
- data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
- data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
- data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
- data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
- data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
- data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +7 -6
- data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
- data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
- data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
- data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
- metadata +80 -60
- /data/docs/{wiki → guides}/API-Reference.md +0 -0
- /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
- /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
- /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
- /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
- /data/docs/{wiki → guides}/Home.md +0 -0
- /data/docs/{wiki → guides}/Implementation-Guide.md +0 -0
- /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Security-Model.md +0 -0
- /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
@@ -0,0 +1,266 @@
|
|
1
|
+
# Relationship Methods
|
2
|
+
|
3
|
+
Here are the methods automatically generated for each relationship type in the new clean API:
|
4
|
+
|
5
|
+
## member_of Relationships
|
6
|
+
|
7
|
+
When you declare:
|
8
|
+
```ruby
|
9
|
+
class Domain < Familia::Horreum
|
10
|
+
member_of Customer, :domains
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
**Generated methods on Domain instances:**
|
15
|
+
- `add_to_customer_domains(customer)` - Add this domain to customer's domains collection
|
16
|
+
- `remove_from_customer_domains(customer)` - Remove this domain from customer's domains collection
|
17
|
+
- `in_customer_domains?(customer)` - Check if this domain is in customer's domains collection
|
18
|
+
|
19
|
+
**Collection << operator support:**
|
20
|
+
```ruby
|
21
|
+
customer.domains << domain # Clean Ruby-like syntax (equivalent to domain.add_to_customer_domains(customer))
|
22
|
+
```
|
23
|
+
|
24
|
+
The method names follow the pattern: `{action}_to_{lowercase_class_name}_{collection_name}`
|
25
|
+
|
26
|
+
## tracked_in Relationships
|
27
|
+
|
28
|
+
### Class-Level Tracking (class_tracked_in)
|
29
|
+
When you declare:
|
30
|
+
```ruby
|
31
|
+
class Customer < Familia::Horreum
|
32
|
+
class_tracked_in :all_customers, score: :created_at
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
**Generated class methods:**
|
37
|
+
- `Customer.add_to_all_customers(customer)` - Add customer to class-level tracking
|
38
|
+
- `Customer.remove_from_all_customers(customer)` - Remove customer from class-level tracking
|
39
|
+
- `Customer.all_customers` - Access the sorted set collection directly
|
40
|
+
|
41
|
+
**Automatic behavior:**
|
42
|
+
- Objects are automatically added to class-level tracking collections when saved
|
43
|
+
- No manual calls required for basic tracking
|
44
|
+
|
45
|
+
### Relationship Tracking (tracked_in with parent class)
|
46
|
+
When you declare:
|
47
|
+
```ruby
|
48
|
+
class User < Familia::Horreum
|
49
|
+
tracked_in Team, :active_users, score: :last_seen
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
**Generated methods:**
|
54
|
+
- Team instance methods for managing the active_users collection
|
55
|
+
- Automatic score calculation based on the provided lambda or field
|
56
|
+
|
57
|
+
## indexed_by Relationships
|
58
|
+
|
59
|
+
The `indexed_by` method creates Redis hash-based indexes for O(1) field lookups with automatic management.
|
60
|
+
|
61
|
+
### Class-Level Indexing (class_indexed_by)
|
62
|
+
When you declare:
|
63
|
+
```ruby
|
64
|
+
class Customer < Familia::Horreum
|
65
|
+
class_indexed_by :email, :email_lookup
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
**Generated methods:**
|
70
|
+
- **Instance methods**: `customer.add_to_class_email_lookup`, `customer.remove_from_class_email_lookup`
|
71
|
+
- **Class methods**: `Customer.email_lookup` (returns hash), `Customer.find_by_email(email)`
|
72
|
+
|
73
|
+
**Automatic behavior:**
|
74
|
+
- Objects are automatically added to class-level indexes when saved
|
75
|
+
- Index updates happen transparently on field changes
|
76
|
+
|
77
|
+
Redis key pattern: `customer:email_lookup`
|
78
|
+
|
79
|
+
### Relationship-Scoped Indexing (indexed_by with parent:)
|
80
|
+
When you declare:
|
81
|
+
```ruby
|
82
|
+
class Domain < Familia::Horreum
|
83
|
+
indexed_by :name, :domain_index, parent: Customer
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
**Generated class methods on Customer:**
|
88
|
+
- `Customer#find_by_name(domain_name)` - Find domain by name within this customer
|
89
|
+
- `Customer#find_all_by_name(domain_names)` - Find multiple domains by names
|
90
|
+
|
91
|
+
Redis key pattern: `domain:domain_index` (all stored at class level for consistency)
|
92
|
+
|
93
|
+
### When to Use Each Context
|
94
|
+
- **Class-level context (`class_indexed_by`)**: Use for system-wide lookups where the field value should be unique across all instances
|
95
|
+
- Examples: email addresses, usernames, API keys
|
96
|
+
- **Relationship context (`parent:` parameter)**: Use for relationship-scoped lookups where the field value is unique within a specific context
|
97
|
+
- Examples: domain names per customer, project names per team
|
98
|
+
|
99
|
+
## Complete Example
|
100
|
+
|
101
|
+
From the relationships example file, you can see the new clean API in action:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
# Domain declares membership in Customer collections
|
105
|
+
class Domain < Familia::Horreum
|
106
|
+
member_of Customer, :domains
|
107
|
+
class_tracked_in :active_domains, score: -> { status == 'active' ? Time.now.to_i : 0 }
|
108
|
+
end
|
109
|
+
|
110
|
+
class Customer < Familia::Horreum
|
111
|
+
class_indexed_by :email, :email_lookup
|
112
|
+
class_tracked_in :all_customers, score: :created_at
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
**Usage with automatic behavior:**
|
117
|
+
```ruby
|
118
|
+
# Create and save objects (automatic indexing and tracking)
|
119
|
+
customer = Customer.new(email: "admin@acme.com", name: "Acme Corp")
|
120
|
+
customer.save # Automatically added to email_lookup and all_customers
|
121
|
+
|
122
|
+
domain = Domain.new(name: "acme.com", status: "active")
|
123
|
+
domain.save # Automatically added to active_domains
|
124
|
+
|
125
|
+
# Clean relationship syntax
|
126
|
+
customer.domains << domain # Ruby-like collection syntax
|
127
|
+
|
128
|
+
# Query relationships
|
129
|
+
domain.in_customer_domains?(customer) # => true
|
130
|
+
customer.domains.member?(domain.identifier) # => true
|
131
|
+
|
132
|
+
# O(1) lookups with automatic management
|
133
|
+
found_id = Customer.email_lookup.get("admin@acme.com")
|
134
|
+
```
|
135
|
+
|
136
|
+
## Method Naming Conventions
|
137
|
+
|
138
|
+
The relationship system uses consistent naming patterns:
|
139
|
+
- **member_of**: `{add_to|remove_from|in}_#{parent_class.downcase}_#{collection_name}`
|
140
|
+
- **class_tracked_in**: `{add_to|remove_from}_#{collection_name}` (class methods)
|
141
|
+
- **class_indexed_by**: `{add_to|remove_from}_class_#{index_name}` (instance methods)
|
142
|
+
- **indexed_by with parent**: `{add_to|remove_from}_#{parent_class.downcase}_#{index_name}` (instance methods)
|
143
|
+
|
144
|
+
## Key Benefits
|
145
|
+
|
146
|
+
- **Automatic management**: Save operations update indexes and tracking automatically
|
147
|
+
- **Ruby-idiomatic**: Use `<<` operator for natural collection syntax
|
148
|
+
- **Consistent storage**: All indexes stored at class level for architectural simplicity
|
149
|
+
- **Clean API**: Removed complex global vs parent conditionals for simpler method generation
|
150
|
+
|
151
|
+
|
152
|
+
## Context Parameter Usage Patterns
|
153
|
+
|
154
|
+
The `context` parameter in `indexed_by` is a fundamental architectural decision that determines index scope and ownership. Here are practical patterns for when to use each approach:
|
155
|
+
|
156
|
+
### Global Context Pattern
|
157
|
+
Use `class_indexed_by` when field values should be unique system-wide:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
class User < Familia::Horreum
|
161
|
+
feature :relationships
|
162
|
+
|
163
|
+
identifier_field :user_id
|
164
|
+
field :user_id, :email, :username
|
165
|
+
|
166
|
+
# System-wide unique email lookup
|
167
|
+
class_indexed_by :email, :email_lookup
|
168
|
+
class_indexed_by :username, :username_lookup
|
169
|
+
end
|
170
|
+
|
171
|
+
# Usage:
|
172
|
+
user.add_to_global_email_lookup
|
173
|
+
found_user_id = User.email_lookup.get("john@example.com")
|
174
|
+
```
|
175
|
+
|
176
|
+
**Redis keys generated**: `global:email_lookup`, `global:username_lookup`
|
177
|
+
|
178
|
+
### Parent Context Pattern
|
179
|
+
Use `parent: SomeClass` when field values are unique within a specific parent context:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class Customer < Familia::Horreum
|
183
|
+
feature :relationships
|
184
|
+
|
185
|
+
identifier_field :custid
|
186
|
+
field :custid, :name
|
187
|
+
sorted_set :domains
|
188
|
+
end
|
189
|
+
|
190
|
+
class Domain < Familia::Horreum
|
191
|
+
feature :relationships
|
192
|
+
|
193
|
+
identifier_field :domain_id
|
194
|
+
field :domain_id, :name, :subdomain
|
195
|
+
|
196
|
+
# Domains are unique per customer (customer can't have duplicate domain names)
|
197
|
+
indexed_by :name, :domain_index, parent: Customer
|
198
|
+
indexed_by :subdomain, :subdomain_index, parent: Customer
|
199
|
+
end
|
200
|
+
|
201
|
+
# Usage:
|
202
|
+
customer = Customer.new(custid: "cust_123")
|
203
|
+
customer.find_by_name("example.com") # Find domain within this customer
|
204
|
+
customer.find_all_by_subdomain(["www", "api"]) # Find multiple subdomains
|
205
|
+
```
|
206
|
+
|
207
|
+
**Redis keys generated**: `customer:cust_123:domain_index`, `customer:cust_123:subdomain_index`
|
208
|
+
|
209
|
+
### Mixed Pattern Example
|
210
|
+
A real-world example showing both patterns:
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
class ApiKey < Familia::Horreum
|
214
|
+
feature :relationships
|
215
|
+
|
216
|
+
identifier_field :key_id
|
217
|
+
field :key_id, :key_hash, :name, :scope
|
218
|
+
|
219
|
+
# API key hashes must be globally unique
|
220
|
+
class_indexed_by :key_hash, :global_key_lookup
|
221
|
+
|
222
|
+
# But key names can be reused across different customers
|
223
|
+
indexed_by :name, :customer_key_lookup, parent: Customer
|
224
|
+
indexed_by :scope, :scope_lookup, parent: Customer
|
225
|
+
end
|
226
|
+
|
227
|
+
# Usage examples:
|
228
|
+
# Global lookup (system-wide unique)
|
229
|
+
ApiKey.key_lookup.get("sha256:abc123...")
|
230
|
+
|
231
|
+
# Scoped lookup (unique per customer)
|
232
|
+
customer = Customer.new(custid: "cust_456")
|
233
|
+
customer.find_by_name("production-api-key")
|
234
|
+
customer.find_all_by_scope(["read", "write"])
|
235
|
+
```
|
236
|
+
|
237
|
+
### Migration Guide
|
238
|
+
If you have existing code with old syntax, here's how to update it:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
# ❌ Old syntax (pre-refactoring)
|
242
|
+
indexed_by :email_lookup, field: :email
|
243
|
+
indexed_by :email, :email_lookup, context: :global
|
244
|
+
tracked_in :global, :all_users, score: :created_at
|
245
|
+
|
246
|
+
# ✅ New syntax - Class-level scope
|
247
|
+
class_indexed_by :email, :email_lookup
|
248
|
+
class_tracked_in :all_users, score: :created_at
|
249
|
+
|
250
|
+
# ✅ New syntax - Relationship scope
|
251
|
+
indexed_by :email, :customer_email_lookup, parent: Customer
|
252
|
+
tracked_in Customer, :user_activity, score: :last_seen
|
253
|
+
```
|
254
|
+
|
255
|
+
**Key Changes**:
|
256
|
+
1. **Class-level relationships**: Use `class_` prefix (`class_tracked_in`, `class_indexed_by`)
|
257
|
+
2. **Relationship-scoped**: Use `parent:` parameter instead of `:global` symbol
|
258
|
+
3. **Automatic management**: Objects automatically added to class-level collections on save
|
259
|
+
4. **Clean syntax**: Collections support `<<` operator for Ruby-like relationship building
|
260
|
+
5. **Simplified storage**: All indexes stored at class level (parent is conceptual only)
|
261
|
+
|
262
|
+
**Behavioral Changes**:
|
263
|
+
- Save operations now automatically update indexes and class-level tracking
|
264
|
+
- No more manual `add_to_*` calls required for basic functionality
|
265
|
+
- `<<` operator works naturally with all collection types
|
266
|
+
- Method generation simplified without complex global/parent conditionals
|
@@ -3,92 +3,100 @@
|
|
3
3
|
# Basic Relationships Example
|
4
4
|
# This example demonstrates the core features of Familia's relationships system
|
5
5
|
|
6
|
-
|
6
|
+
require 'familia'
|
7
7
|
|
8
8
|
# Configure Familia for the example
|
9
|
+
# Note: Individual models can specify logical_database if needed
|
9
10
|
Familia.configure do |config|
|
10
|
-
config.
|
11
|
+
config.uri = 'redis://localhost:6379/'
|
11
12
|
end
|
12
13
|
|
13
|
-
puts
|
14
|
+
puts '=== Familia Relationships Basic Example ==='
|
14
15
|
puts
|
15
16
|
|
16
17
|
# Define our model classes
|
17
18
|
class Customer < Familia::Horreum
|
19
|
+
logical_database 15 # Use test database
|
18
20
|
feature :relationships
|
19
21
|
|
20
22
|
identifier_field :custid
|
21
|
-
field :custid
|
23
|
+
field :custid
|
24
|
+
field :name
|
25
|
+
field :email
|
26
|
+
field :plan
|
22
27
|
|
23
28
|
# Define collections for tracking relationships
|
24
29
|
set :domains # Simple set of domain IDs
|
25
30
|
list :projects # Ordered list of project IDs
|
26
31
|
sorted_set :activity # Activity feed with timestamps
|
27
32
|
|
28
|
-
# Create indexes for fast lookups
|
29
|
-
|
30
|
-
|
33
|
+
# Create indexes for fast lookups (using class_ prefix for class-level)
|
34
|
+
class_indexed_by :email, :email_lookup # i.e. Customer.email_lookup
|
35
|
+
class_indexed_by :plan, :plan_lookup # i.e. Customer.plan_lookup
|
31
36
|
|
32
|
-
# Track in
|
33
|
-
|
34
|
-
|
35
|
-
def created_at
|
36
|
-
Time.now.to_i
|
37
|
-
end
|
37
|
+
# Track in class-level collections (using class_ prefix for class-level)
|
38
|
+
class_tracked_in :all_customers, score: :created_at # i.e. Customer.all_customers
|
38
39
|
end
|
39
40
|
|
40
41
|
class Domain < Familia::Horreum
|
42
|
+
logical_database 15 # Use test database
|
41
43
|
feature :relationships
|
42
44
|
|
43
45
|
identifier_field :domain_id
|
44
|
-
field :domain_id
|
46
|
+
field :domain_id
|
47
|
+
field :name
|
48
|
+
field :dns_zone
|
49
|
+
field :status
|
45
50
|
|
46
51
|
# Declare membership in customer collections
|
47
|
-
member_of Customer, :domains
|
52
|
+
member_of Customer, :domains #, type: :set
|
48
53
|
|
49
|
-
# Track domains by status
|
50
|
-
|
51
|
-
|
54
|
+
# Track domains by status (using class_ prefix for class-level)
|
55
|
+
class_tracked_in :active_domains,
|
56
|
+
score: -> { status == 'active' ? Time.now.to_i : 0 }
|
52
57
|
end
|
53
58
|
|
54
59
|
class Project < Familia::Horreum
|
60
|
+
logical_database 15 # Use test database
|
55
61
|
feature :relationships
|
56
62
|
|
57
63
|
identifier_field :project_id
|
58
|
-
field :project_id
|
64
|
+
field :project_id
|
65
|
+
field :name
|
66
|
+
field :priority
|
59
67
|
|
60
68
|
# Member of customer projects list (ordered)
|
61
69
|
member_of Customer, :projects, type: :list
|
62
70
|
end
|
63
71
|
|
64
|
-
puts
|
72
|
+
puts '=== 1. Basic Object Creation ==='
|
65
73
|
|
66
74
|
# Create some sample objects
|
67
75
|
customer = Customer.new(
|
68
|
-
custid: "cust_#{SecureRandom.hex(4)}",
|
69
|
-
name:
|
70
|
-
email:
|
71
|
-
plan:
|
76
|
+
# custid: "cust_#{SecureRandom.hex(4)}",
|
77
|
+
name: 'Acme Corporation',
|
78
|
+
email: 'admin@acme.com',
|
79
|
+
plan: 'enterprise'
|
72
80
|
)
|
73
81
|
|
74
82
|
domain1 = Domain.new(
|
75
|
-
domain_id: "dom_#{SecureRandom.hex(4)}",
|
76
|
-
name:
|
77
|
-
dns_zone:
|
78
|
-
status:
|
83
|
+
# domain_id: "dom_#{SecureRandom.hex(4)}",
|
84
|
+
name: 'acme.com',
|
85
|
+
dns_zone: 'acme.com.',
|
86
|
+
status: 'active'
|
79
87
|
)
|
80
88
|
|
81
89
|
domain2 = Domain.new(
|
82
|
-
domain_id: "dom_#{SecureRandom.hex(4)}",
|
83
|
-
name:
|
84
|
-
dns_zone:
|
85
|
-
status:
|
90
|
+
# domain_id: "dom_#{SecureRandom.hex(4)}",
|
91
|
+
name: 'staging.acme.com',
|
92
|
+
dns_zone: 'staging.acme.com.',
|
93
|
+
status: 'active'
|
86
94
|
)
|
87
95
|
|
88
96
|
project = Project.new(
|
89
|
-
project_id: "proj_#{SecureRandom.hex(4)}",
|
90
|
-
name:
|
91
|
-
priority:
|
97
|
+
# project_id: "proj_#{SecureRandom.hex(4)}",
|
98
|
+
name: 'Website Redesign',
|
99
|
+
priority: 'high'
|
92
100
|
)
|
93
101
|
|
94
102
|
puts "✓ Created customer: #{customer.name} (#{customer.custid})"
|
@@ -96,49 +104,44 @@ puts "✓ Created domains: #{domain1.name}, #{domain2.name}"
|
|
96
104
|
puts "✓ Created project: #{project.name}"
|
97
105
|
puts
|
98
106
|
|
99
|
-
puts
|
100
|
-
|
101
|
-
# Add objects to indexed lookups
|
102
|
-
Customer.add_to_email_lookup(customer)
|
103
|
-
Customer.add_to_plan_lookup(customer)
|
104
|
-
puts "✓ Added customer to email and plan indexes"
|
107
|
+
puts '=== 2. Establishing Relationships ==='
|
105
108
|
|
106
|
-
#
|
107
|
-
|
108
|
-
puts
|
109
|
+
# Save objects to automatically update indexes and class-level tracking
|
110
|
+
customer.save # Automatically adds to email_lookup, plan_lookup, and all_customers
|
111
|
+
puts '✓ Customer automatically added to indexes and tracking on save'
|
109
112
|
|
110
|
-
# Establish member_of relationships
|
111
|
-
|
112
|
-
customer.domains.
|
113
|
+
# Establish member_of relationships using clean << operator syntax
|
114
|
+
customer.domains << domain1 # Clean Ruby-like syntax
|
115
|
+
customer.domains << domain2 # Same as domain1.add_to_customer_domains(customer)
|
116
|
+
customer.projects << project # Same as project.add_to_customer_projects(customer)
|
113
117
|
|
114
|
-
|
115
|
-
|
118
|
+
puts '✓ Established domain ownership relationships using << operator'
|
119
|
+
puts '✓ Established project ownership relationships using << operator'
|
116
120
|
|
117
|
-
|
118
|
-
|
121
|
+
# Save domains to automatically add them to class-level tracking
|
122
|
+
domain1.save # Automatically adds to active_domains if status == 'active'
|
123
|
+
domain2.save # Automatically adds to active_domains if status == 'active'
|
124
|
+
puts '✓ Domains automatically added to status tracking on save'
|
125
|
+
puts
|
119
126
|
|
120
|
-
puts
|
121
|
-
puts "✓ Established project ownership relationships"
|
127
|
+
puts '=== 3. Querying Relationships ==='
|
122
128
|
|
123
|
-
|
124
|
-
|
125
|
-
Domain.add_to_active_domains(domain2)
|
126
|
-
puts "✓ Added domains to active tracking"
|
127
|
-
puts
|
129
|
+
record = Customer.get_by_email('admin@acme.com')
|
130
|
+
puts "Email lookup: #{record&.custid || 'not found'}"
|
128
131
|
|
129
|
-
|
132
|
+
Customer.get_by_plan('enterprise') # raises MoreThanOne
|
130
133
|
|
131
|
-
|
132
|
-
|
133
|
-
puts "Email lookup for #{customer.email}: #{found_customer_id}"
|
134
|
+
results = Customer.find_by_email('admin@acme.com')
|
135
|
+
puts "Email lookup: #{results&.size} found"
|
134
136
|
|
135
|
-
|
136
|
-
puts "Enterprise
|
137
|
+
results = Customer.find_by_plan('enterprise')
|
138
|
+
puts "Enterprise lookup: #{results&.size} found"
|
139
|
+
puts
|
137
140
|
|
138
141
|
# Test membership queries
|
139
142
|
puts "\nDomain membership checks:"
|
140
|
-
puts " #{domain1.name} belongs to customer? #{domain1.in_customer_domains?(customer
|
141
|
-
puts " #{domain2.name} belongs to customer? #{domain2.in_customer_domains?(customer
|
143
|
+
puts " #{domain1.name} belongs to customer? #{domain1.in_customer_domains?(customer)}"
|
144
|
+
puts " #{domain2.name} belongs to customer? #{domain2.in_customer_domains?(customer)}"
|
142
145
|
|
143
146
|
puts "\nCustomer collections:"
|
144
147
|
puts " Customer has #{customer.domains.size} domains"
|
@@ -147,69 +150,37 @@ puts " Domain IDs: #{customer.domains.members}"
|
|
147
150
|
puts " Project IDs: #{customer.projects.members}"
|
148
151
|
|
149
152
|
# Test tracked_in collections
|
150
|
-
all_customers_count = Customer.
|
151
|
-
puts "\
|
153
|
+
all_customers_count = Customer.values.size
|
154
|
+
puts "\nClass-level tracking:"
|
152
155
|
puts " Total customers in system: #{all_customers_count}"
|
153
156
|
|
154
157
|
active_domains_count = Domain.active_domains.size
|
155
158
|
puts " Active domains in system: #{active_domains_count}"
|
156
159
|
puts
|
157
160
|
|
158
|
-
puts
|
161
|
+
puts '=== 4. Range Queries ==='
|
159
162
|
|
160
163
|
# Get recent customers (last 24 hours)
|
161
|
-
yesterday = (Time.now - 24
|
162
|
-
recent_customers = Customer.
|
164
|
+
yesterday = (Time.now - (24 * 3600)).to_i # 24 hours ago
|
165
|
+
recent_customers = Customer.values.rangebyscore(yesterday, '+inf')
|
163
166
|
puts "Recent customers (last 24h): #{recent_customers.size}"
|
164
167
|
|
165
168
|
# Get all active domains by score
|
166
|
-
active_domain_scores = Domain.active_domains.
|
167
|
-
puts
|
168
|
-
active_domain_scores.
|
169
|
-
puts " #{domain_id}: active since #{Time.at(timestamp.to_i)}"
|
169
|
+
active_domain_scores = Domain.active_domains.rangebyscore(1, '+inf', with_scores: true)
|
170
|
+
puts 'Active domains with timestamps:'
|
171
|
+
active_domain_scores.each_slice(2) do |domain_id, timestamp|
|
172
|
+
puts " #{domain_id}: active since #{Time.at(timestamp.to_i)} #{timestamp.inspect}"
|
170
173
|
end
|
171
174
|
puts
|
172
175
|
|
173
|
-
puts "=== 5. Batch Operations ==="
|
174
|
-
|
175
|
-
# Create additional test data
|
176
|
-
additional_customers = []
|
177
|
-
3.times do |i|
|
178
|
-
cust = Customer.new(
|
179
|
-
custid: "batch_cust_#{i}",
|
180
|
-
name: "Customer #{i}",
|
181
|
-
email: "customer#{i}@example.com",
|
182
|
-
plan: i.even? ? "basic" : "premium"
|
183
|
-
)
|
184
|
-
additional_customers << cust
|
185
|
-
|
186
|
-
# Add to indexes and tracking
|
187
|
-
Customer.add_to_email_lookup(cust)
|
188
|
-
Customer.add_to_plan_lookup(cust)
|
189
|
-
Customer.add_to_all_customers(cust)
|
190
|
-
end
|
191
|
-
|
192
|
-
puts "✓ Created and indexed #{additional_customers.size} additional customers"
|
193
176
|
|
194
|
-
|
195
|
-
basic_customers = Customer.plan_lookup.get("basic")
|
196
|
-
premium_customers = Customer.plan_lookup.get("premium")
|
197
|
-
enterprise_customers = Customer.plan_lookup.get("enterprise")
|
198
|
-
|
199
|
-
puts "\nCustomer distribution by plan:"
|
200
|
-
puts " Basic: #{basic_customers ? 1 : 0} customers"
|
201
|
-
puts " Premium: #{premium_customers ? 1 : 0} customers"
|
202
|
-
puts " Enterprise: #{enterprise_customers ? 1 : 0} customers"
|
203
|
-
puts
|
204
|
-
|
205
|
-
puts "=== 6. Relationship Cleanup ==="
|
177
|
+
puts '=== 6. Relationship Cleanup ==='
|
206
178
|
|
207
179
|
# Remove relationships
|
208
|
-
puts
|
180
|
+
puts 'Cleaning up relationships...'
|
209
181
|
|
210
182
|
# Remove from member_of relationships
|
211
|
-
domain1.remove_from_customer_domains(customer
|
212
|
-
customer.domains.remove(domain1.identifier)
|
183
|
+
domain1.remove_from_customer_domains(customer)
|
213
184
|
puts "✓ Removed #{domain1.name} from customer domains"
|
214
185
|
|
215
186
|
# Remove from tracking collections
|
@@ -222,52 +193,14 @@ puts " Customer domains: #{customer.domains.size}"
|
|
222
193
|
puts " Active domains: #{Domain.active_domains.size}"
|
223
194
|
puts
|
224
195
|
|
225
|
-
puts
|
226
|
-
|
227
|
-
# Demonstrate basic score encoding for permissions
|
228
|
-
class DocumentAccess
|
229
|
-
# Permission flags (powers of 2)
|
230
|
-
READ = 1
|
231
|
-
WRITE = 2
|
232
|
-
DELETE = 4
|
233
|
-
ADMIN = 8
|
234
|
-
|
235
|
-
def self.encode_permissions(timestamp, permissions)
|
236
|
-
"#{timestamp}.#{permissions}".to_f
|
237
|
-
end
|
238
|
-
|
239
|
-
def self.decode_permissions(score)
|
240
|
-
parts = score.to_s.split('.')
|
241
|
-
timestamp = parts[0].to_i
|
242
|
-
permissions = parts[1] ? parts[1].to_i : 0
|
243
|
-
[timestamp, permissions]
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
# Example permission encoding
|
248
|
-
now = Time.now.to_i
|
249
|
-
read_write_permissions = DocumentAccess::READ | DocumentAccess::WRITE # 3
|
250
|
-
admin_permissions = DocumentAccess::ADMIN # 8
|
251
|
-
|
252
|
-
encoded_score = DocumentAccess.encode_permissions(now, read_write_permissions)
|
253
|
-
timestamp, permissions = DocumentAccess.decode_permissions(encoded_score)
|
254
|
-
|
255
|
-
puts "Permission encoding example:"
|
256
|
-
puts " Original: timestamp=#{now}, permissions=#{read_write_permissions}"
|
257
|
-
puts " Encoded score: #{encoded_score}"
|
258
|
-
puts " Decoded: timestamp=#{timestamp}, permissions=#{permissions}"
|
259
|
-
puts " Has read access: #{(permissions & DocumentAccess::READ) != 0}"
|
260
|
-
puts " Has write access: #{(permissions & DocumentAccess::WRITE) != 0}"
|
261
|
-
puts " Has delete access: #{(permissions & DocumentAccess::DELETE) != 0}"
|
262
|
-
puts
|
263
|
-
|
264
|
-
puts "=== Example Complete! ==="
|
196
|
+
puts '=== Example Complete! ==='
|
265
197
|
puts
|
266
|
-
puts
|
267
|
-
puts
|
268
|
-
puts
|
269
|
-
puts
|
270
|
-
puts
|
271
|
-
puts
|
198
|
+
puts 'Key takeaways:'
|
199
|
+
puts '• class_tracked_in: Automatic class-level collections updated on save'
|
200
|
+
puts '• class_indexed_by: Automatic class-level indexes updated on save'
|
201
|
+
puts '• member_of: Use << operator for clean Ruby-like collection syntax'
|
202
|
+
puts '• indexed_by with parent:: Use for relationship-scoped indexes'
|
203
|
+
puts '• Save operations: Automatically update indexes and class-level tracking'
|
204
|
+
puts '• << operator: Works naturally with all collection types (sets, lists, sorted sets)'
|
272
205
|
puts
|
273
|
-
puts
|
206
|
+
puts 'See docs/wiki/Relationships-Guide.md for comprehensive documentation'
|
data/familia.gemspec
CHANGED
@@ -19,10 +19,10 @@ Gem::Specification.new do |spec|
|
|
19
19
|
|
20
20
|
spec.required_ruby_version = Gem::Requirement.new('>= 3.4')
|
21
21
|
|
22
|
-
spec.add_dependency 'benchmark'
|
23
|
-
spec.add_dependency 'connection_pool'
|
24
|
-
spec.add_dependency 'csv'
|
25
|
-
spec.add_dependency 'logger'
|
22
|
+
spec.add_dependency 'benchmark', '~> 0.1'
|
23
|
+
spec.add_dependency 'connection_pool', '~> 2.4'
|
24
|
+
spec.add_dependency 'csv', '~> 3.1'
|
25
|
+
spec.add_dependency 'logger', '~> 1.6'
|
26
26
|
spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
|
27
27
|
spec.add_dependency 'stringio', '~> 3.1.1'
|
28
28
|
spec.add_dependency 'uri-valkey', '~> 1.4'
|