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,210 @@
|
|
1
|
+
# Relationship methods
|
2
|
+
|
3
|
+
here are the methods automatically generated for each relationship type:
|
4
|
+
|
5
|
+
member_of Relationships
|
6
|
+
|
7
|
+
When you declare:
|
8
|
+
class Domain < Familia::Horreum
|
9
|
+
member_of Customer, :domains, type: :set
|
10
|
+
end
|
11
|
+
|
12
|
+
Generated methods on Domain instances:
|
13
|
+
- add_to_customer_domains(customer_id) - Add this domain to customer's domains collection
|
14
|
+
- remove_from_customer_domains(customer_id) - Remove this domain from customer's domains collection
|
15
|
+
- in_customer_domains?(customer_id) - Check if this domain is in customer's domains collection
|
16
|
+
|
17
|
+
The method names follow the pattern: {action}_to_{lowercase_class_name}_{collection_name}
|
18
|
+
|
19
|
+
tracked_in Relationships
|
20
|
+
|
21
|
+
When you declare:
|
22
|
+
class Customer < Familia::Horreum
|
23
|
+
tracked_in :all_customers, type: :sorted_set, score: :created_at
|
24
|
+
end
|
25
|
+
|
26
|
+
Generated class methods:
|
27
|
+
- Customer.add_to_all_customers(customer) - Add customer to global tracking
|
28
|
+
- Customer.remove_from_all_customers(customer) - Remove customer from global tracking
|
29
|
+
- Customer.all_customers - Access the sorted set collection directly
|
30
|
+
|
31
|
+
For scoped tracking (with class prefix):
|
32
|
+
tracked_in :global, :active_users, score: :last_seen
|
33
|
+
Generates: Customer.add_to_active_users(customer) and Customer.active_users
|
34
|
+
|
35
|
+
indexed_by Relationships
|
36
|
+
|
37
|
+
The `indexed_by` method creates Redis hash-based indexes for O(1) field lookups. The `context` parameter determines index ownership and scope.
|
38
|
+
|
39
|
+
**Global Context (Shared Index)**
|
40
|
+
When you declare:
|
41
|
+
```ruby
|
42
|
+
class Customer < Familia::Horreum
|
43
|
+
indexed_by :email, :email_lookup, context: :global
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Generated class methods:
|
48
|
+
- Customer.add_to_email_lookup(customer) - Add customer to global email index
|
49
|
+
- Customer.remove_from_email_lookup(customer) - Remove customer from global email index
|
50
|
+
- Customer.email_lookup - Access the global hash index directly (supports .get(email))
|
51
|
+
|
52
|
+
Redis key pattern: `global:email_lookup`
|
53
|
+
|
54
|
+
**Class Context (Per-Instance Index)**
|
55
|
+
When you declare:
|
56
|
+
```ruby
|
57
|
+
class Domain < Familia::Horreum
|
58
|
+
indexed_by :name, :domain_index, context: Customer
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Generated class methods on Customer:
|
63
|
+
- Customer.find_by_name(domain_name) - Find domain by name within this customer
|
64
|
+
- Customer.find_all_by_name(domain_names) - Find multiple domains by names
|
65
|
+
|
66
|
+
Redis key pattern: `customer:123:domain_index` (per customer instance)
|
67
|
+
|
68
|
+
**When to Use Each Context**
|
69
|
+
- **Global context (`:global`)**: Use for system-wide lookups where the field value should be unique across all instances
|
70
|
+
- Examples: email addresses, usernames, API keys
|
71
|
+
- **Class context (e.g., `Customer`)**: Use for scoped lookups where the field value is unique within a specific parent object
|
72
|
+
- Examples: domain names per customer, project names per team
|
73
|
+
|
74
|
+
Example from the Codebase
|
75
|
+
|
76
|
+
From the relationships example file, you can see this in action:
|
77
|
+
|
78
|
+
# Domain declares membership in Customer collections
|
79
|
+
class Domain < Familia::Horreum
|
80
|
+
member_of Customer, :domains, type: :set
|
81
|
+
end
|
82
|
+
|
83
|
+
# This generates these methods on Domain instances:
|
84
|
+
domain.add_to_customer_domains(customer.custid) # Add to relationship
|
85
|
+
domain.remove_from_customer_domains(customer.custid) # Remove from relationship
|
86
|
+
domain.in_customer_domains?(customer.custid) # Query membership
|
87
|
+
|
88
|
+
# For tracked_in relationships:
|
89
|
+
Customer.add_to_all_customers(customer) # Class method
|
90
|
+
Customer.all_customers.range(0, -1) # Direct collection access
|
91
|
+
|
92
|
+
# For indexed_by relationships:
|
93
|
+
Customer.add_to_email_lookup(customer) # Class method
|
94
|
+
Customer.email_lookup.get("user@example.com") # O(1) lookup
|
95
|
+
|
96
|
+
Method Naming Conventions
|
97
|
+
|
98
|
+
The relationship system uses consistent naming patterns:
|
99
|
+
- member_of: {add_to|remove_from|in}_#{parent_class.downcase}_#{collection_name}
|
100
|
+
- tracked_in: {add_to|remove_from}_#{collection_name} (class methods)
|
101
|
+
- indexed_by: {add_to|remove_from}_#{index_name} (class methods)
|
102
|
+
|
103
|
+
This automatic method generation creates a clean, predictable API that handles both the Redis operations and maintains referential consistency
|
104
|
+
across related objects.
|
105
|
+
|
106
|
+
|
107
|
+
## Context Parameter Usage Patterns
|
108
|
+
|
109
|
+
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:
|
110
|
+
|
111
|
+
### Global Context Pattern
|
112
|
+
Use `context: :global` when field values should be unique system-wide:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class User < Familia::Horreum
|
116
|
+
feature :relationships
|
117
|
+
|
118
|
+
identifier_field :user_id
|
119
|
+
field :user_id, :email, :username
|
120
|
+
|
121
|
+
# System-wide unique email lookup
|
122
|
+
indexed_by :email, :email_lookup, context: :global
|
123
|
+
indexed_by :username, :username_lookup, context: :global
|
124
|
+
end
|
125
|
+
|
126
|
+
# Usage:
|
127
|
+
User.add_to_email_lookup(user)
|
128
|
+
found_user_id = User.email_lookup.get("john@example.com")
|
129
|
+
```
|
130
|
+
|
131
|
+
**Redis keys generated**: `global:email_lookup`, `global:username_lookup`
|
132
|
+
|
133
|
+
### Class Context Pattern
|
134
|
+
Use `context: SomeClass` when field values are unique within a specific parent context:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class Customer < Familia::Horreum
|
138
|
+
feature :relationships
|
139
|
+
|
140
|
+
identifier_field :custid
|
141
|
+
field :custid, :name
|
142
|
+
sorted_set :domains
|
143
|
+
end
|
144
|
+
|
145
|
+
class Domain < Familia::Horreum
|
146
|
+
feature :relationships
|
147
|
+
|
148
|
+
identifier_field :domain_id
|
149
|
+
field :domain_id, :name, :subdomain
|
150
|
+
|
151
|
+
# Domains are unique per customer (customer can't have duplicate domain names)
|
152
|
+
indexed_by :name, :domain_index, context: Customer
|
153
|
+
indexed_by :subdomain, :subdomain_index, context: Customer
|
154
|
+
end
|
155
|
+
|
156
|
+
# Usage:
|
157
|
+
customer = Customer.new(custid: "cust_123")
|
158
|
+
customer.find_by_name("example.com") # Find domain within this customer
|
159
|
+
customer.find_all_by_subdomain(["www", "api"]) # Find multiple subdomains
|
160
|
+
```
|
161
|
+
|
162
|
+
**Redis keys generated**: `customer:cust_123:domain_index`, `customer:cust_123:subdomain_index`
|
163
|
+
|
164
|
+
### Mixed Pattern Example
|
165
|
+
A real-world example showing both patterns:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
class ApiKey < Familia::Horreum
|
169
|
+
feature :relationships
|
170
|
+
|
171
|
+
identifier_field :key_id
|
172
|
+
field :key_id, :key_hash, :name, :scope
|
173
|
+
|
174
|
+
# API key hashes must be globally unique
|
175
|
+
indexed_by :key_hash, :global_key_lookup, context: :global
|
176
|
+
|
177
|
+
# But key names can be reused across different customers
|
178
|
+
indexed_by :name, :customer_key_lookup, context: Customer
|
179
|
+
indexed_by :scope, :scope_lookup, context: Customer
|
180
|
+
end
|
181
|
+
|
182
|
+
# Usage examples:
|
183
|
+
# Global lookup (system-wide unique)
|
184
|
+
ApiKey.key_lookup.get("sha256:abc123...")
|
185
|
+
|
186
|
+
# Scoped lookup (unique per customer)
|
187
|
+
customer = Customer.new(custid: "cust_456")
|
188
|
+
customer.find_by_name("production-api-key")
|
189
|
+
customer.find_all_by_scope(["read", "write"])
|
190
|
+
```
|
191
|
+
|
192
|
+
### Migration Guide
|
193
|
+
If you have existing code with incorrect syntax, here's how to fix it:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
# ❌ Old incorrect syntax
|
197
|
+
indexed_by :email_lookup, field: :email
|
198
|
+
|
199
|
+
# ✅ New correct syntax - Global scope
|
200
|
+
indexed_by :email, :email_lookup, context: :global
|
201
|
+
|
202
|
+
# ✅ New correct syntax - Class scope
|
203
|
+
indexed_by :email, :customer_email_lookup, context: Customer
|
204
|
+
```
|
205
|
+
|
206
|
+
**Key Differences**:
|
207
|
+
1. Parameter order: `indexed_by(field, index_name, context:)`
|
208
|
+
2. The `field:` named parameter is now positional
|
209
|
+
3. The `context:` parameter is required and determines scope
|
210
|
+
4. Index name comes second (allows same field to have multiple indexes)
|