dzql 0.5.33 → 0.6.1
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.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +309 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +653 -0
- package/docs/project-setup.md +456 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +166 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,929 +0,0 @@
|
|
|
1
|
-
# Many-to-Many Relationships
|
|
2
|
-
|
|
3
|
-
First-class support for many-to-many relationships with automatic junction table management.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
DZQL provides built-in support for many-to-many (M2M) relationships through junction tables. Define the relationship once in your entity configuration, and DZQL handles:
|
|
8
|
-
|
|
9
|
-
- Junction table synchronization
|
|
10
|
-
- Atomic updates in single API calls
|
|
11
|
-
- Automatic expansion in get/search operations
|
|
12
|
-
- Real-time broadcasts with complete state
|
|
13
|
-
|
|
14
|
-
## Benefits
|
|
15
|
-
|
|
16
|
-
- **Single API Call** - No more N+1 calls for relationship management
|
|
17
|
-
- **Atomic Operations** - All changes in one transaction
|
|
18
|
-
- **Consistent API** - Works like regular fields
|
|
19
|
-
- **Less Boilerplate** - No custom toggle functions needed
|
|
20
|
-
- **Performance Control** - Optional expansion (off by default)
|
|
21
|
-
|
|
22
|
-
## Generic vs Compiled Operations
|
|
23
|
-
|
|
24
|
-
M2M support works in **both** modes:
|
|
25
|
-
|
|
26
|
-
### Generic Operations (Runtime)
|
|
27
|
-
- Uses `dzql.generic_save()` and dynamic SQL
|
|
28
|
-
- Interprets M2M config at runtime (~5-10ms overhead per relationship)
|
|
29
|
-
- Works immediately after `register_entity()` call
|
|
30
|
-
- Good for development and entities with simple M2M
|
|
31
|
-
|
|
32
|
-
### Compiled Operations (v0.3.1+) - RECOMMENDED
|
|
33
|
-
- Generates **static SQL** at compile time
|
|
34
|
-
- **50-100x faster** - zero interpretation overhead
|
|
35
|
-
- All table/column names are literals (PostgreSQL optimizes fully)
|
|
36
|
-
- Recommended for production and complex M2M scenarios
|
|
37
|
-
|
|
38
|
-
See [Compiler Guide](../compiler/README.md) for compilation workflow.
|
|
39
|
-
|
|
40
|
-
## Quick Example
|
|
41
|
-
|
|
42
|
-
### Setup
|
|
43
|
-
|
|
44
|
-
```sql
|
|
45
|
-
-- Create tables
|
|
46
|
-
CREATE TABLE brands (
|
|
47
|
-
id serial PRIMARY KEY,
|
|
48
|
-
org_id integer REFERENCES organisations(id),
|
|
49
|
-
name text NOT NULL
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
CREATE TABLE tags (
|
|
53
|
-
id serial PRIMARY KEY,
|
|
54
|
-
name text NOT NULL UNIQUE,
|
|
55
|
-
color text
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
-- Junction table
|
|
59
|
-
CREATE TABLE brand_tags (
|
|
60
|
-
brand_id integer REFERENCES brands(id) ON DELETE CASCADE,
|
|
61
|
-
tag_id integer REFERENCES tags(id) ON DELETE CASCADE,
|
|
62
|
-
PRIMARY KEY (brand_id, tag_id)
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
-- Register with M2M support
|
|
66
|
-
SELECT dzql.register_entity(
|
|
67
|
-
'brands',
|
|
68
|
-
'name',
|
|
69
|
-
ARRAY['name'],
|
|
70
|
-
'{}', false, '{}', '{}', '{}',
|
|
71
|
-
'{
|
|
72
|
-
"many_to_many": {
|
|
73
|
-
"tags": {
|
|
74
|
-
"junction_table": "brand_tags",
|
|
75
|
-
"local_key": "brand_id",
|
|
76
|
-
"foreign_key": "tag_id",
|
|
77
|
-
"target_entity": "tags",
|
|
78
|
-
"id_field": "tag_ids",
|
|
79
|
-
"expand": false
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}',
|
|
83
|
-
'{}'
|
|
84
|
-
);
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Client Usage
|
|
88
|
-
|
|
89
|
-
```javascript
|
|
90
|
-
// Create brand with tags in one call
|
|
91
|
-
const brand = await api.save_brands({
|
|
92
|
-
data: {
|
|
93
|
-
name: "Premium Brand",
|
|
94
|
-
org_id: 1,
|
|
95
|
-
tag_ids: [1, 2, 3] // Junction table synced automatically!
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Response includes tag IDs
|
|
100
|
-
console.log(brand.tag_ids) // [1, 2, 3]
|
|
101
|
-
|
|
102
|
-
// Get brand - tag_ids always included
|
|
103
|
-
const retrieved = await api.get_brands({ id: brand.id })
|
|
104
|
-
console.log(retrieved.tag_ids) // [1, 2, 3]
|
|
105
|
-
|
|
106
|
-
// Update tags - single atomic operation
|
|
107
|
-
await api.save_brands({
|
|
108
|
-
data: {
|
|
109
|
-
id: brand.id,
|
|
110
|
-
tag_ids: [2, 3, 4] // Removes 1, keeps 2&3, adds 4
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
// Remove all tags
|
|
115
|
-
await api.save_brands({
|
|
116
|
-
data: {
|
|
117
|
-
id: brand.id,
|
|
118
|
-
tag_ids: [] // Clears all tags
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Configuration
|
|
124
|
-
|
|
125
|
-
M2M relationships are configured in the `graph_rules` parameter (9th parameter) of `register_entity()`:
|
|
126
|
-
|
|
127
|
-
```sql
|
|
128
|
-
SELECT dzql.register_entity(
|
|
129
|
-
'table_name',
|
|
130
|
-
'label_field',
|
|
131
|
-
ARRAY['searchable_fields'],
|
|
132
|
-
'{}', -- fk_includes
|
|
133
|
-
false, -- soft_delete
|
|
134
|
-
'{}', -- temporal_fields
|
|
135
|
-
'{}', -- notification_paths
|
|
136
|
-
'{}', -- permission_paths
|
|
137
|
-
'{
|
|
138
|
-
"many_to_many": {
|
|
139
|
-
"relationship_name": {
|
|
140
|
-
"junction_table": "table_name",
|
|
141
|
-
"local_key": "local_fk_column",
|
|
142
|
-
"foreign_key": "foreign_fk_column",
|
|
143
|
-
"target_entity": "target_table",
|
|
144
|
-
"id_field": "field_name_for_ids",
|
|
145
|
-
"expand": false
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}',
|
|
149
|
-
'{}' -- field_defaults
|
|
150
|
-
);
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Configuration Options
|
|
154
|
-
|
|
155
|
-
| Option | Required | Description | Example |
|
|
156
|
-
|--------|----------|-------------|---------|
|
|
157
|
-
| `junction_table` | Yes | Name of junction table | `"brand_tags"` |
|
|
158
|
-
| `local_key` | Yes | FK column pointing to this entity | `"brand_id"` |
|
|
159
|
-
| `foreign_key` | Yes | FK column pointing to target entity | `"tag_id"` |
|
|
160
|
-
| `target_entity` | Yes | Name of target entity table | `"tags"` |
|
|
161
|
-
| `id_field` | Yes | Field name for ID array in API | `"tag_ids"` |
|
|
162
|
-
| `expand` | No | Include full objects (default: false) | `false` or `true` |
|
|
163
|
-
|
|
164
|
-
### Naming Convention for `id_field`
|
|
165
|
-
|
|
166
|
-
**Important:** The `id_field` should use the **singular** form of the target entity, not plural:
|
|
167
|
-
|
|
168
|
-
| Target Entity | Correct `id_field` | Wrong |
|
|
169
|
-
|---------------|-------------------|-------|
|
|
170
|
-
| `tags` | `tag_ids` | `tags_ids` |
|
|
171
|
-
| `roles` | `role_ids` | `roles_ids` |
|
|
172
|
-
| `categories` | `category_ids` | `categories_ids` |
|
|
173
|
-
| `users` | `user_ids` | `users_ids` |
|
|
174
|
-
|
|
175
|
-
This convention matches common ORM patterns and is more readable:
|
|
176
|
-
- `tag_ids: [1, 2, 3]` reads as "tag IDs"
|
|
177
|
-
- `tags_ids: [1, 2, 3]` reads awkwardly as "tags IDs"
|
|
178
|
-
|
|
179
|
-
Using the wrong naming will cause the M2M sync to fail because the generated code looks for a field name that doesn't match what clients send.
|
|
180
|
-
|
|
181
|
-
### The `expand` Flag
|
|
182
|
-
|
|
183
|
-
Controls whether full related objects are included in responses:
|
|
184
|
-
|
|
185
|
-
**`expand: false`** (default - recommended for performance):
|
|
186
|
-
```javascript
|
|
187
|
-
{
|
|
188
|
-
id: 1,
|
|
189
|
-
name: "My Brand",
|
|
190
|
-
tag_ids: [1, 2, 3] // Just IDs
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
**`expand: true`** (for detail views):
|
|
195
|
-
```javascript
|
|
196
|
-
{
|
|
197
|
-
id: 1,
|
|
198
|
-
name: "My Brand",
|
|
199
|
-
tag_ids: [1, 2, 3], // IDs included
|
|
200
|
-
tags: [ // Full objects included
|
|
201
|
-
{ id: 1, name: "Premium", color: "#FFD700" },
|
|
202
|
-
{ id: 2, name: "Popular", color: "#3B82F6" },
|
|
203
|
-
{ id: 3, name: "New", color: "#8B5CF6" }
|
|
204
|
-
]
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**Performance Impact:**
|
|
209
|
-
- `expand: false` - Single query for ID array (fast)
|
|
210
|
-
- `expand: true` - Additional JOIN per relationship (slower)
|
|
211
|
-
|
|
212
|
-
**Recommendation:** Use `false` for list views, `true` for detail views (or fetch tags separately when needed).
|
|
213
|
-
|
|
214
|
-
## Junction Table Requirements
|
|
215
|
-
|
|
216
|
-
Your junction table must have:
|
|
217
|
-
|
|
218
|
-
1. **Two foreign key columns** pointing to the related tables
|
|
219
|
-
2. **Composite primary key** or unique constraint on both columns
|
|
220
|
-
3. **ON DELETE CASCADE** (recommended) for automatic cleanup
|
|
221
|
-
|
|
222
|
-
```sql
|
|
223
|
-
CREATE TABLE brand_tags (
|
|
224
|
-
brand_id integer NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
|
|
225
|
-
tag_id integer NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
226
|
-
PRIMARY KEY (brand_id, tag_id) -- Composite PK prevents duplicates
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
-- Index for reverse lookups (optional but recommended)
|
|
230
|
-
CREATE INDEX idx_brand_tags_tag_id ON brand_tags(tag_id);
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
## API Operations
|
|
234
|
-
|
|
235
|
-
### Create with Relationships
|
|
236
|
-
|
|
237
|
-
```javascript
|
|
238
|
-
// Create brand with tags in single call
|
|
239
|
-
const brand = await api.save_brands({
|
|
240
|
-
data: {
|
|
241
|
-
name: "New Brand",
|
|
242
|
-
tag_ids: [1, 2, 3]
|
|
243
|
-
}
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
// Response
|
|
247
|
-
{
|
|
248
|
-
id: 5,
|
|
249
|
-
name: "New Brand",
|
|
250
|
-
tag_ids: [1, 2, 3]
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Update Relationships
|
|
255
|
-
|
|
256
|
-
```javascript
|
|
257
|
-
// Add and remove tags atomically
|
|
258
|
-
await api.save_brands({
|
|
259
|
-
data: {
|
|
260
|
-
id: 5,
|
|
261
|
-
tag_ids: [2, 3, 4, 5] // Remove 1, keep 2&3, add 4&5
|
|
262
|
-
}
|
|
263
|
-
})
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### Remove All Relationships
|
|
267
|
-
|
|
268
|
-
```javascript
|
|
269
|
-
// Empty array removes all
|
|
270
|
-
await api.save_brands({
|
|
271
|
-
data: {
|
|
272
|
-
id: 5,
|
|
273
|
-
tag_ids: [] // Clears all tags
|
|
274
|
-
}
|
|
275
|
-
})
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### Leave Relationships Unchanged
|
|
279
|
-
|
|
280
|
-
```javascript
|
|
281
|
-
// Omit the field to not touch relationships
|
|
282
|
-
await api.save_brands({
|
|
283
|
-
data: {
|
|
284
|
-
id: 5,
|
|
285
|
-
name: "Updated Name"
|
|
286
|
-
// tag_ids not included - tags unchanged
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Get with Relationships
|
|
292
|
-
|
|
293
|
-
```javascript
|
|
294
|
-
// Get always includes tag_ids
|
|
295
|
-
const brand = await api.get_brands({ id: 5 })
|
|
296
|
-
|
|
297
|
-
console.log(brand.tag_ids) // [2, 3, 4, 5]
|
|
298
|
-
|
|
299
|
-
// If expand: true in config
|
|
300
|
-
console.log(brand.tags) // [{id: 2, ...}, {id: 3, ...}, ...]
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Search with Relationships
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
// Search includes tag_ids for each result
|
|
307
|
-
const results = await api.search_brands({ limit: 10 })
|
|
308
|
-
|
|
309
|
-
results.data.forEach(brand => {
|
|
310
|
-
console.log(brand.tag_ids) // Array of IDs
|
|
311
|
-
})
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
## Multiple M2M Relationships
|
|
315
|
-
|
|
316
|
-
You can define multiple M2M relationships on a single entity:
|
|
317
|
-
|
|
318
|
-
```sql
|
|
319
|
-
SELECT dzql.register_entity(
|
|
320
|
-
'resources',
|
|
321
|
-
'title',
|
|
322
|
-
ARRAY['title'],
|
|
323
|
-
'{}', false, '{}', '{}', '{}',
|
|
324
|
-
'{
|
|
325
|
-
"many_to_many": {
|
|
326
|
-
"tags": {
|
|
327
|
-
"junction_table": "resource_tags",
|
|
328
|
-
"local_key": "resource_id",
|
|
329
|
-
"foreign_key": "tag_id",
|
|
330
|
-
"target_entity": "tags",
|
|
331
|
-
"id_field": "tag_ids",
|
|
332
|
-
"expand": false
|
|
333
|
-
},
|
|
334
|
-
"collaborators": {
|
|
335
|
-
"junction_table": "resource_collaborators",
|
|
336
|
-
"local_key": "resource_id",
|
|
337
|
-
"foreign_key": "user_id",
|
|
338
|
-
"target_entity": "users",
|
|
339
|
-
"id_field": "collaborator_ids",
|
|
340
|
-
"expand": true
|
|
341
|
-
},
|
|
342
|
-
"categories": {
|
|
343
|
-
"junction_table": "resource_categories",
|
|
344
|
-
"local_key": "resource_id",
|
|
345
|
-
"foreign_key": "category_id",
|
|
346
|
-
"target_entity": "categories",
|
|
347
|
-
"id_field": "category_ids",
|
|
348
|
-
"expand": false
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}',
|
|
352
|
-
'{}'
|
|
353
|
-
);
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
Client usage:
|
|
357
|
-
|
|
358
|
-
```javascript
|
|
359
|
-
await api.save_resources({
|
|
360
|
-
data: {
|
|
361
|
-
title: "My Resource",
|
|
362
|
-
tag_ids: [1, 2],
|
|
363
|
-
collaborator_ids: [10, 20],
|
|
364
|
-
category_ids: [5]
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
// All three relationships synced atomically!
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
## Implementation Details
|
|
372
|
-
|
|
373
|
-
### How Junction Sync Works
|
|
374
|
-
|
|
375
|
-
When you call `save_entity()` with an M2M ID field:
|
|
376
|
-
|
|
377
|
-
1. **INSERT/UPDATE** the main entity
|
|
378
|
-
2. **DELETE** relationships not in new list
|
|
379
|
-
3. **INSERT** new relationships (ON CONFLICT DO NOTHING)
|
|
380
|
-
4. **QUERY** final state and return with ID arrays
|
|
381
|
-
|
|
382
|
-
All in a single transaction - atomic!
|
|
383
|
-
|
|
384
|
-
### SQL Generated
|
|
385
|
-
|
|
386
|
-
For `save_brands()` with `tag_ids: [1, 2, 3]`:
|
|
387
|
-
|
|
388
|
-
```sql
|
|
389
|
-
-- 1. Insert/update brand
|
|
390
|
-
INSERT INTO brands (...) VALUES (...);
|
|
391
|
-
|
|
392
|
-
-- 2. Delete tags not in [1,2,3]
|
|
393
|
-
DELETE FROM brand_tags
|
|
394
|
-
WHERE brand_id = 5
|
|
395
|
-
AND tag_id <> ALL(ARRAY[1,2,3]);
|
|
396
|
-
|
|
397
|
-
-- 3. Insert new tags
|
|
398
|
-
INSERT INTO brand_tags (brand_id, tag_id)
|
|
399
|
-
VALUES (5, 1), (5, 2), (5, 3)
|
|
400
|
-
ON CONFLICT DO NOTHING;
|
|
401
|
-
|
|
402
|
-
-- 4. Fetch final state
|
|
403
|
-
SELECT jsonb_agg(tag_id) FROM brand_tags WHERE brand_id = 5;
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
### Null Handling
|
|
407
|
-
|
|
408
|
-
| Input | Behavior |
|
|
409
|
-
|-------|----------|
|
|
410
|
-
| `tag_ids: [1, 2]` | Sync to exactly [1, 2] |
|
|
411
|
-
| `tag_ids: []` | Remove all relationships |
|
|
412
|
-
| `tag_ids: null` | Leave unchanged (same as omitted) |
|
|
413
|
-
| Field omitted | Leave unchanged |
|
|
414
|
-
|
|
415
|
-
## Comparison to Manual Approach
|
|
416
|
-
|
|
417
|
-
### Before DZQL M2M Support
|
|
418
|
-
|
|
419
|
-
**Database:**
|
|
420
|
-
```sql
|
|
421
|
-
-- Custom toggle function (40+ lines)
|
|
422
|
-
CREATE FUNCTION toggle_resource_tag(
|
|
423
|
-
p_user_id INT,
|
|
424
|
-
p_resource_id INT,
|
|
425
|
-
p_tag_id INT
|
|
426
|
-
) RETURNS JSONB AS $$ ... $$;
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
**Client:**
|
|
430
|
-
```javascript
|
|
431
|
-
// 1. Save resource
|
|
432
|
-
const resource = await api.save_resources({
|
|
433
|
-
data: { title: "Room A" }
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
// 2. Calculate delta
|
|
437
|
-
const toAdd = [1, 2, 3]
|
|
438
|
-
const toRemove = [4]
|
|
439
|
-
|
|
440
|
-
// 3. Make N API calls
|
|
441
|
-
for (const tagId of toAdd) {
|
|
442
|
-
await api.toggle_resource_tag({
|
|
443
|
-
p_resource_id: resource.id,
|
|
444
|
-
p_tag_id: tagId
|
|
445
|
-
})
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
**Issues:**
|
|
450
|
-
- N+1 API calls
|
|
451
|
-
- Not atomic
|
|
452
|
-
- Custom function required
|
|
453
|
-
- Verbose client code
|
|
454
|
-
|
|
455
|
-
### After DZQL M2M Support
|
|
456
|
-
|
|
457
|
-
**Database:**
|
|
458
|
-
```sql
|
|
459
|
-
-- Just entity registration
|
|
460
|
-
SELECT dzql.register_entity(
|
|
461
|
-
'resources',
|
|
462
|
-
...,
|
|
463
|
-
'{"many_to_many": {"tags": {...}}}'
|
|
464
|
-
);
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
**Client:**
|
|
468
|
-
```javascript
|
|
469
|
-
// Single atomic call
|
|
470
|
-
const resource = await api.save_resources({
|
|
471
|
-
data: {
|
|
472
|
-
title: "Room A",
|
|
473
|
-
tag_ids: [1, 2, 3]
|
|
474
|
-
}
|
|
475
|
-
})
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
**Benefits:**
|
|
479
|
-
- 1 API call
|
|
480
|
-
- Atomic
|
|
481
|
-
- No custom function
|
|
482
|
-
- Clean code
|
|
483
|
-
|
|
484
|
-
## Common Patterns
|
|
485
|
-
|
|
486
|
-
### Tags
|
|
487
|
-
|
|
488
|
-
```sql
|
|
489
|
-
"tags": {
|
|
490
|
-
"junction_table": "resource_tags",
|
|
491
|
-
"local_key": "resource_id",
|
|
492
|
-
"foreign_key": "tag_id",
|
|
493
|
-
"target_entity": "tags",
|
|
494
|
-
"id_field": "tag_ids",
|
|
495
|
-
"expand": false
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
### Collaborators/Team Members
|
|
500
|
-
|
|
501
|
-
```sql
|
|
502
|
-
"collaborators": {
|
|
503
|
-
"junction_table": "project_collaborators",
|
|
504
|
-
"local_key": "project_id",
|
|
505
|
-
"foreign_key": "user_id",
|
|
506
|
-
"target_entity": "users",
|
|
507
|
-
"id_field": "collaborator_ids",
|
|
508
|
-
"expand": true // Probably want full user objects
|
|
509
|
-
}
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
### Categories/Taxonomies
|
|
513
|
-
|
|
514
|
-
```sql
|
|
515
|
-
"categories": {
|
|
516
|
-
"junction_table": "item_categories",
|
|
517
|
-
"local_key": "item_id",
|
|
518
|
-
"foreign_key": "category_id",
|
|
519
|
-
"target_entity": "categories",
|
|
520
|
-
"id_field": "category_ids",
|
|
521
|
-
"expand": false
|
|
522
|
-
}
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
### Permissions/Roles
|
|
526
|
-
|
|
527
|
-
```sql
|
|
528
|
-
"roles": {
|
|
529
|
-
"junction_table": "user_roles",
|
|
530
|
-
"local_key": "user_id",
|
|
531
|
-
"foreign_key": "role_id",
|
|
532
|
-
"target_entity": "roles",
|
|
533
|
-
"id_field": "role_ids",
|
|
534
|
-
"expand": true
|
|
535
|
-
}
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
## Advanced Usage
|
|
539
|
-
|
|
540
|
-
### Fetching Tags Separately
|
|
541
|
-
|
|
542
|
-
When using `expand: false`, fetch related entities when needed:
|
|
543
|
-
|
|
544
|
-
```javascript
|
|
545
|
-
// Get brands (with tag IDs only)
|
|
546
|
-
const brands = await api.search_brands({ limit: 10 })
|
|
547
|
-
|
|
548
|
-
// For a specific brand, fetch full tag details
|
|
549
|
-
const brand = brands.data[0]
|
|
550
|
-
const tags = await api.search_tags({
|
|
551
|
-
p_filters: { id: { in: brand.tag_ids } }
|
|
552
|
-
})
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### Filtering by M2M Relationships
|
|
556
|
-
|
|
557
|
-
To find all brands with a specific tag:
|
|
558
|
-
|
|
559
|
-
```javascript
|
|
560
|
-
// Custom SQL or use raw query
|
|
561
|
-
const brandsWithTag = await sql`
|
|
562
|
-
SELECT b.*
|
|
563
|
-
FROM brands b
|
|
564
|
-
JOIN brand_tags bt ON bt.brand_id = b.id
|
|
565
|
-
WHERE bt.tag_id = 5
|
|
566
|
-
`
|
|
567
|
-
|
|
568
|
-
// Or create a custom function for this pattern
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
### Junction Tables with Extra Fields
|
|
572
|
-
|
|
573
|
-
DZQL's M2M currently supports simple junction tables. For junction tables with additional fields (e.g., `position`, `added_at`):
|
|
574
|
-
|
|
575
|
-
**Option 1:** Model junction as entity
|
|
576
|
-
```sql
|
|
577
|
-
-- Register the junction table as its own entity
|
|
578
|
-
SELECT dzql.register_entity(
|
|
579
|
-
'resource_tags',
|
|
580
|
-
'id',
|
|
581
|
-
ARRAY[],
|
|
582
|
-
'{
|
|
583
|
-
"resource": "resources",
|
|
584
|
-
"tag": "tags"
|
|
585
|
-
}',
|
|
586
|
-
...
|
|
587
|
-
);
|
|
588
|
-
|
|
589
|
-
-- Then use regular FK relationships
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
**Option 2:** Use custom function
|
|
593
|
-
```sql
|
|
594
|
-
CREATE FUNCTION add_tag_with_position(
|
|
595
|
-
p_user_id INT,
|
|
596
|
-
p_resource_id INT,
|
|
597
|
-
p_tag_id INT,
|
|
598
|
-
p_position INT
|
|
599
|
-
) RETURNS JSONB AS $$ ... $$;
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
## Permissions
|
|
603
|
-
|
|
604
|
-
M2M operations respect the entity's update permissions:
|
|
605
|
-
|
|
606
|
-
```sql
|
|
607
|
-
SELECT dzql.register_entity(
|
|
608
|
-
'resources',
|
|
609
|
-
'title',
|
|
610
|
-
ARRAY['title'],
|
|
611
|
-
'{}', false, '{}', '{}',
|
|
612
|
-
'{
|
|
613
|
-
"view": [],
|
|
614
|
-
"create": [],
|
|
615
|
-
"update": ["@owner_id"], -- User must own resource to change tags
|
|
616
|
-
"delete": ["@owner_id"]
|
|
617
|
-
}',
|
|
618
|
-
'{
|
|
619
|
-
"many_to_many": {
|
|
620
|
-
"tags": { ... }
|
|
621
|
-
}
|
|
622
|
-
}'
|
|
623
|
-
);
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
If a user can update the resource, they can change its tags. No separate permission check for M2M.
|
|
627
|
-
|
|
628
|
-
## Error Handling
|
|
629
|
-
|
|
630
|
-
### Non-existent IDs
|
|
631
|
-
|
|
632
|
-
If you provide tag IDs that don't exist:
|
|
633
|
-
|
|
634
|
-
```javascript
|
|
635
|
-
await api.save_brands({
|
|
636
|
-
data: {
|
|
637
|
-
id: 1,
|
|
638
|
-
tag_ids: [1, 999, 3] // 999 doesn't exist
|
|
639
|
-
}
|
|
640
|
-
})
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
**Behavior:** `ON CONFLICT DO NOTHING` silently skips invalid IDs. Only valid relationships are created.
|
|
644
|
-
|
|
645
|
-
**Recommendation:** Validate IDs client-side or use lookup APIs.
|
|
646
|
-
|
|
647
|
-
### Foreign Key Violations
|
|
648
|
-
|
|
649
|
-
Junction table foreign keys enforce referential integrity:
|
|
650
|
-
|
|
651
|
-
```sql
|
|
652
|
-
CREATE TABLE brand_tags (
|
|
653
|
-
brand_id integer REFERENCES brands(id) ON DELETE CASCADE,
|
|
654
|
-
tag_id integer REFERENCES tags(id) ON DELETE CASCADE,
|
|
655
|
-
...
|
|
656
|
-
);
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
- ✅ Deleting a brand cascades to junction table
|
|
660
|
-
- ✅ Deleting a tag cascades to junction table
|
|
661
|
-
- ✅ Database ensures data integrity
|
|
662
|
-
|
|
663
|
-
## Real-time Updates
|
|
664
|
-
|
|
665
|
-
M2M changes are broadcast via DZQL's event system:
|
|
666
|
-
|
|
667
|
-
```javascript
|
|
668
|
-
// User A saves brand with new tags
|
|
669
|
-
await api.save_brands({
|
|
670
|
-
data: { id: 1, tag_ids: [1, 2, 3] }
|
|
671
|
-
})
|
|
672
|
-
|
|
673
|
-
// User B listening to brands entity receives:
|
|
674
|
-
{
|
|
675
|
-
op: "update",
|
|
676
|
-
table_name: "brands",
|
|
677
|
-
after: {
|
|
678
|
-
id: 1,
|
|
679
|
-
name: "Brand Name",
|
|
680
|
-
tag_ids: [1, 2, 3] // Complete state
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
Broadcasts include complete state, so subscribers always have consistent data.
|
|
686
|
-
|
|
687
|
-
## Performance Considerations
|
|
688
|
-
|
|
689
|
-
### Search Performance
|
|
690
|
-
|
|
691
|
-
With `expand: false` (default):
|
|
692
|
-
- **Fast** - One additional query per record for ID array
|
|
693
|
-
- Recommended for list views
|
|
694
|
-
|
|
695
|
-
With `expand: true`:
|
|
696
|
-
- **Slower** - Additional JOIN per record per relationship
|
|
697
|
-
- Use sparingly, or only for detail views
|
|
698
|
-
|
|
699
|
-
### Optimization Tips
|
|
700
|
-
|
|
701
|
-
1. **Index junction tables:**
|
|
702
|
-
```sql
|
|
703
|
-
CREATE INDEX idx_brand_tags_brand_id ON brand_tags(brand_id);
|
|
704
|
-
CREATE INDEX idx_brand_tags_tag_id ON brand_tags(tag_id);
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
2. **Limit array sizes** - Consider max tags per entity (e.g., 50)
|
|
708
|
-
|
|
709
|
-
3. **Use expand: false** for listings, fetch full objects only when needed
|
|
710
|
-
|
|
711
|
-
4. **Cache tag definitions** client-side if tags are static
|
|
712
|
-
|
|
713
|
-
## Example: Complete Implementation
|
|
714
|
-
|
|
715
|
-
```sql
|
|
716
|
-
-- ============================================================================
|
|
717
|
-
-- Tags & Resources with M2M
|
|
718
|
-
-- ============================================================================
|
|
719
|
-
|
|
720
|
-
-- Tags table
|
|
721
|
-
CREATE TABLE tags (
|
|
722
|
-
id serial PRIMARY KEY,
|
|
723
|
-
name text NOT NULL UNIQUE,
|
|
724
|
-
color text,
|
|
725
|
-
description text
|
|
726
|
-
);
|
|
727
|
-
|
|
728
|
-
-- Resources table
|
|
729
|
-
CREATE TABLE resources (
|
|
730
|
-
id serial PRIMARY KEY,
|
|
731
|
-
org_id integer REFERENCES organisations(id),
|
|
732
|
-
title text NOT NULL,
|
|
733
|
-
description text,
|
|
734
|
-
owner_id integer REFERENCES users(id)
|
|
735
|
-
);
|
|
736
|
-
|
|
737
|
-
-- Junction table
|
|
738
|
-
CREATE TABLE resource_tags (
|
|
739
|
-
resource_id integer NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
740
|
-
tag_id integer NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
741
|
-
PRIMARY KEY (resource_id, tag_id)
|
|
742
|
-
);
|
|
743
|
-
|
|
744
|
-
CREATE INDEX idx_resource_tags_tag_id ON resource_tags(tag_id);
|
|
745
|
-
|
|
746
|
-
-- Register tags entity (public, simple)
|
|
747
|
-
SELECT dzql.register_entity(
|
|
748
|
-
'tags',
|
|
749
|
-
'name',
|
|
750
|
-
ARRAY['name', 'description'],
|
|
751
|
-
'{}', false, '{}', '{}',
|
|
752
|
-
'{
|
|
753
|
-
"view": [],
|
|
754
|
-
"create": [],
|
|
755
|
-
"update": [],
|
|
756
|
-
"delete": []
|
|
757
|
-
}',
|
|
758
|
-
'{}',
|
|
759
|
-
'{}'
|
|
760
|
-
);
|
|
761
|
-
|
|
762
|
-
-- Register resources entity with M2M tags
|
|
763
|
-
SELECT dzql.register_entity(
|
|
764
|
-
'resources',
|
|
765
|
-
'title',
|
|
766
|
-
ARRAY['title', 'description'],
|
|
767
|
-
'{"org": "organisations"}',
|
|
768
|
-
false,
|
|
769
|
-
'{}',
|
|
770
|
-
'{}',
|
|
771
|
-
'{
|
|
772
|
-
"view": [],
|
|
773
|
-
"create": [],
|
|
774
|
-
"update": ["@owner_id"],
|
|
775
|
-
"delete": ["@owner_id"]
|
|
776
|
-
}',
|
|
777
|
-
'{
|
|
778
|
-
"many_to_many": {
|
|
779
|
-
"tags": {
|
|
780
|
-
"junction_table": "resource_tags",
|
|
781
|
-
"local_key": "resource_id",
|
|
782
|
-
"foreign_key": "tag_id",
|
|
783
|
-
"target_entity": "tags",
|
|
784
|
-
"id_field": "tag_ids",
|
|
785
|
-
"expand": false
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}',
|
|
789
|
-
'{"owner_id": "@user_id", "created_at": "@now"}'
|
|
790
|
-
);
|
|
791
|
-
|
|
792
|
-
-- Sample tags
|
|
793
|
-
INSERT INTO tags (name, color, description) VALUES
|
|
794
|
-
('Important', '#EF4444', 'High priority items'),
|
|
795
|
-
('In Progress', '#F59E0B', 'Currently being worked on'),
|
|
796
|
-
('Completed', '#10B981', 'Finished items')
|
|
797
|
-
ON CONFLICT (name) DO NOTHING;
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
**Client usage:**
|
|
801
|
-
|
|
802
|
-
```javascript
|
|
803
|
-
// Create resource with tags - single call!
|
|
804
|
-
const resource = await api.save_resources({
|
|
805
|
-
data: {
|
|
806
|
-
title: "Conference Room A",
|
|
807
|
-
description: "Main conference room",
|
|
808
|
-
tag_ids: [1, 2] // Important + In Progress
|
|
809
|
-
// owner_id auto-populated from field defaults
|
|
810
|
-
// created_at auto-populated from field defaults
|
|
811
|
-
}
|
|
812
|
-
})
|
|
813
|
-
|
|
814
|
-
// Response
|
|
815
|
-
{
|
|
816
|
-
id: 1,
|
|
817
|
-
title: "Conference Room A",
|
|
818
|
-
description: "Main conference room",
|
|
819
|
-
owner_id: 123,
|
|
820
|
-
created_at: "2025-11-20T15:00:00Z",
|
|
821
|
-
tag_ids: [1, 2]
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// Update status by changing tags
|
|
825
|
-
await api.save_resources({
|
|
826
|
-
data: {
|
|
827
|
-
id: 1,
|
|
828
|
-
tag_ids: [3] // Change to Completed
|
|
829
|
-
}
|
|
830
|
-
})
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
## Migration Guide
|
|
834
|
-
|
|
835
|
-
### From Manual M2M to DZQL M2M
|
|
836
|
-
|
|
837
|
-
If you're currently using custom toggle functions:
|
|
838
|
-
|
|
839
|
-
**Step 1:** Create junction table (if you haven't)
|
|
840
|
-
```sql
|
|
841
|
-
CREATE TABLE resource_tags (
|
|
842
|
-
resource_id integer REFERENCES resources(id) ON DELETE CASCADE,
|
|
843
|
-
tag_id integer REFERENCES tags(id) ON DELETE CASCADE,
|
|
844
|
-
PRIMARY KEY (resource_id, tag_id)
|
|
845
|
-
);
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
**Step 2:** Update entity registration to include M2M config
|
|
849
|
-
|
|
850
|
-
**Step 3:** Remove custom toggle functions (optional - both can coexist)
|
|
851
|
-
|
|
852
|
-
**Step 4:** Update client code to use `tag_ids` array instead of toggle calls
|
|
853
|
-
|
|
854
|
-
**Step 5:** Deploy updated functions
|
|
855
|
-
|
|
856
|
-
## Comparison to Other ORMs
|
|
857
|
-
|
|
858
|
-
DZQL's M2M support is similar to:
|
|
859
|
-
|
|
860
|
-
### Rails ActiveRecord
|
|
861
|
-
```ruby
|
|
862
|
-
resource.tag_ids = [1, 2, 3]
|
|
863
|
-
resource.save # Junction table synced
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
### Django ORM
|
|
867
|
-
```python
|
|
868
|
-
resource.tags.set([tag1, tag2, tag3])
|
|
869
|
-
```
|
|
870
|
-
|
|
871
|
-
### Prisma
|
|
872
|
-
```javascript
|
|
873
|
-
await prisma.resource.update({
|
|
874
|
-
where: { id: 1 },
|
|
875
|
-
data: {
|
|
876
|
-
tags: { set: [{ id: 1 }, { id: 2 }] }
|
|
877
|
-
}
|
|
878
|
-
})
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
DZQL provides similar ergonomics with the added benefit of real-time synchronization and row-level security.
|
|
882
|
-
|
|
883
|
-
## Known Limitations
|
|
884
|
-
|
|
885
|
-
1. **Composite Primary Keys** - Currently assumes single PK (uses first PK column)
|
|
886
|
-
2. **Junction Table Fields** - No support for extra fields on junction table
|
|
887
|
-
3. **Ordering** - No built-in support for position/order in relationships
|
|
888
|
-
|
|
889
|
-
For these advanced cases, model the junction table as its own entity or use custom functions.
|
|
890
|
-
|
|
891
|
-
## Troubleshooting
|
|
892
|
-
|
|
893
|
-
### IDs not appearing in response
|
|
894
|
-
|
|
895
|
-
**Check:** Is M2M configured in entity registration?
|
|
896
|
-
```sql
|
|
897
|
-
SELECT many_to_many FROM dzql.entities WHERE table_name = 'brands';
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
### Junction table not syncing
|
|
901
|
-
|
|
902
|
-
**Check:** Is the `id_field` spelled correctly?
|
|
903
|
-
```javascript
|
|
904
|
-
// Config says "tag_ids"
|
|
905
|
-
"id_field": "tag_ids"
|
|
906
|
-
|
|
907
|
-
// Client must use same name
|
|
908
|
-
tag_ids: [1, 2, 3] // ✅ Correct
|
|
909
|
-
tags: [1, 2, 3] // ❌ Wrong field name
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
### Foreign key violations
|
|
913
|
-
|
|
914
|
-
**Check:** Do the IDs exist in the target table?
|
|
915
|
-
```sql
|
|
916
|
-
SELECT id FROM tags WHERE id IN (1, 2, 3);
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
### Performance issues in search
|
|
920
|
-
|
|
921
|
-
**Check:** Is `expand: true` on a heavily queried entity?
|
|
922
|
-
|
|
923
|
-
**Solution:** Change to `expand: false` and fetch full objects separately when needed.
|
|
924
|
-
|
|
925
|
-
## See Also
|
|
926
|
-
|
|
927
|
-
- [Field Defaults](./field-defaults.md) - Auto-populate ownership and timestamps
|
|
928
|
-
- [Custom Functions](./custom-functions.md) - Advanced business logic
|
|
929
|
-
- [Graph Rules](../reference/api.md#graph-rules) - Automatic relationship management
|