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
package/docs/reference/api.md
DELETED
|
@@ -1,1373 +0,0 @@
|
|
|
1
|
-
# DZQL API Reference
|
|
2
|
-
|
|
3
|
-
Complete API documentation for DZQL framework. For tutorials, see [Getting Started Tutorial](../getting-started/tutorial.md). For AI development guide, see [Claude Guide](../for-ai/claude-guide.md).
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [The 5 Operations](#the-5-operations)
|
|
8
|
-
- [Entity Registration](#entity-registration)
|
|
9
|
-
- [Search Operators](#search-operators)
|
|
10
|
-
- [Graph Rules](#graph-rules)
|
|
11
|
-
- [Permission & Notification Paths](#permission--notification-paths)
|
|
12
|
-
- [Custom Functions](#custom-functions)
|
|
13
|
-
- [Authentication](#authentication)
|
|
14
|
-
- [Real-time Events](#real-time-events)
|
|
15
|
-
- [Live Query Subscriptions](#live-query-subscriptions)
|
|
16
|
-
- [Temporal Relationships](#temporal-relationships)
|
|
17
|
-
- [Error Messages](#error-messages)
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## The 5 Operations
|
|
22
|
-
|
|
23
|
-
Every registered entity automatically gets these 5 operations via the proxy API:
|
|
24
|
-
|
|
25
|
-
### GET - Retrieve Single Record
|
|
26
|
-
|
|
27
|
-
Fetch a single record by primary key with foreign keys dereferenced.
|
|
28
|
-
|
|
29
|
-
**Client:**
|
|
30
|
-
```javascript
|
|
31
|
-
const record = await ws.api.get.{entity}({id: 1});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**Server:**
|
|
35
|
-
```javascript
|
|
36
|
-
const record = await db.api.get.{entity}({id: 1}, userId);
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**Parameters:**
|
|
40
|
-
| Field | Type | Required | Description |
|
|
41
|
-
|-------|------|----------|-------------|
|
|
42
|
-
| `id` | any | yes | Primary key value |
|
|
43
|
-
| `on_date` | string | no | Temporal filtering (ISO 8601 date) |
|
|
44
|
-
|
|
45
|
-
**Returns:** Object with all fields + dereferenced FKs
|
|
46
|
-
|
|
47
|
-
**Throws:** `"record not found"` if not exists
|
|
48
|
-
|
|
49
|
-
**Example:**
|
|
50
|
-
```javascript
|
|
51
|
-
const venue = await ws.api.get.venues({id: 1});
|
|
52
|
-
// {id: 1, name: "MSG", org: {id: 3, name: "Org"}, sites: [...]}
|
|
53
|
-
|
|
54
|
-
// With temporal filtering
|
|
55
|
-
const historical = await ws.api.get.venues({id: 1, on_date: '2023-01-01'});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
### SAVE - Create or Update (Upsert)
|
|
61
|
-
|
|
62
|
-
Insert new record (no `id`) or update existing (with `id`).
|
|
63
|
-
|
|
64
|
-
**Client:**
|
|
65
|
-
```javascript
|
|
66
|
-
const record = await ws.api.save.{entity}({...fields});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**Server:**
|
|
70
|
-
```javascript
|
|
71
|
-
const record = await db.api.save.{entity}({...fields}, userId);
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Parameters:**
|
|
75
|
-
| Field | Type | Required | Description |
|
|
76
|
-
|-------|------|----------|-------------|
|
|
77
|
-
| `id` | any | no | Omit for insert, include for update |
|
|
78
|
-
| ...fields | any | varies | Entity-specific fields |
|
|
79
|
-
|
|
80
|
-
**Returns:** Created/updated record
|
|
81
|
-
|
|
82
|
-
**Behavior:**
|
|
83
|
-
- **No `id`**: INSERT new record
|
|
84
|
-
- **With `id`**: UPDATE existing record (partial update supported)
|
|
85
|
-
- Triggers graph rules if configured
|
|
86
|
-
- Generates real-time event
|
|
87
|
-
|
|
88
|
-
**Example:**
|
|
89
|
-
```javascript
|
|
90
|
-
// Insert
|
|
91
|
-
const venue = await ws.api.save.venues({
|
|
92
|
-
name: 'Madison Square Garden',
|
|
93
|
-
address: 'NYC',
|
|
94
|
-
org_id: 1
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Update (partial)
|
|
98
|
-
const updated = await ws.api.save.venues({
|
|
99
|
-
id: 1,
|
|
100
|
-
name: 'Updated Name' // Only updates name
|
|
101
|
-
});
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
### DELETE - Remove Record
|
|
107
|
-
|
|
108
|
-
Delete a record by primary key.
|
|
109
|
-
|
|
110
|
-
**Client:**
|
|
111
|
-
```javascript
|
|
112
|
-
const result = await ws.api.delete.{entity}({id: 1});
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
**Server:**
|
|
116
|
-
```javascript
|
|
117
|
-
const result = await db.api.delete.{entity}({id: 1}, userId);
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**Parameters:**
|
|
121
|
-
| Field | Type | Required | Description |
|
|
122
|
-
|-------|------|----------|-------------|
|
|
123
|
-
| `id` | any | yes | Primary key value |
|
|
124
|
-
|
|
125
|
-
**Returns:** Deleted record
|
|
126
|
-
|
|
127
|
-
**Behavior:**
|
|
128
|
-
- Hard delete (unless soft delete configured)
|
|
129
|
-
- Triggers graph rules if configured
|
|
130
|
-
- Generates real-time event
|
|
131
|
-
|
|
132
|
-
**Example:**
|
|
133
|
-
```javascript
|
|
134
|
-
const deleted = await ws.api.delete.venues({id: 1});
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
### LOOKUP - Autocomplete/Typeahead
|
|
140
|
-
|
|
141
|
-
Get label-value pairs for autocomplete inputs.
|
|
142
|
-
|
|
143
|
-
**Client:**
|
|
144
|
-
```javascript
|
|
145
|
-
const options = await ws.api.lookup.{entity}({p_filter: 'search'});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Server:**
|
|
149
|
-
```javascript
|
|
150
|
-
const options = await db.api.lookup.{entity}({p_filter: 'search'}, userId);
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Parameters:**
|
|
154
|
-
| Field | Type | Required | Description |
|
|
155
|
-
|-------|------|----------|-------------|
|
|
156
|
-
| `p_filter` | string | no | Search term (matches label field) |
|
|
157
|
-
|
|
158
|
-
**Returns:** Array of `{label, value}` objects
|
|
159
|
-
|
|
160
|
-
**Example:**
|
|
161
|
-
```javascript
|
|
162
|
-
const options = await ws.api.lookup.venues({p_filter: 'madison'});
|
|
163
|
-
// [{label: "Madison Square Garden", value: 1}, ...]
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
### SEARCH - Advanced Search with Pagination
|
|
169
|
-
|
|
170
|
-
Search with filters, sorting, and pagination.
|
|
171
|
-
|
|
172
|
-
**Client:**
|
|
173
|
-
```javascript
|
|
174
|
-
const results = await ws.api.search.{entity}({
|
|
175
|
-
filters: {...},
|
|
176
|
-
sort: {field, order},
|
|
177
|
-
page: 1,
|
|
178
|
-
limit: 25
|
|
179
|
-
});
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
**Server:**
|
|
183
|
-
```javascript
|
|
184
|
-
const results = await db.api.search.{entity}({...}, userId);
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
**Parameters:**
|
|
188
|
-
| Field | Type | Required | Description |
|
|
189
|
-
|-------|------|----------|-------------|
|
|
190
|
-
| `filters` | object | no | See [Search Operators](#search-operators) |
|
|
191
|
-
| `sort` | object | no | `{field: 'name', order: 'asc' | 'desc'}` |
|
|
192
|
-
| `page` | number | no | Page number (1-indexed, default: 1) |
|
|
193
|
-
| `limit` | number | no | Records per page (default: 25) |
|
|
194
|
-
|
|
195
|
-
**Returns:**
|
|
196
|
-
```javascript
|
|
197
|
-
{
|
|
198
|
-
data: [...], // Array of records
|
|
199
|
-
total: 100, // Total matching records
|
|
200
|
-
page: 1, // Current page
|
|
201
|
-
limit: 25 // Records per page
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
**Example:**
|
|
206
|
-
```javascript
|
|
207
|
-
const results = await ws.api.search.venues({
|
|
208
|
-
filters: {
|
|
209
|
-
city: 'New York',
|
|
210
|
-
capacity: {gte: 1000, lt: 5000},
|
|
211
|
-
name: {ilike: '%garden%'},
|
|
212
|
-
_search: 'madison' // Text search across searchable fields
|
|
213
|
-
},
|
|
214
|
-
sort: {field: 'name', order: 'asc'},
|
|
215
|
-
page: 1,
|
|
216
|
-
limit: 25
|
|
217
|
-
});
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
---
|
|
221
|
-
|
|
222
|
-
## Entity Registration
|
|
223
|
-
|
|
224
|
-
Register an entity to enable all 5 operations via `dzql.register_entity()`.
|
|
225
|
-
|
|
226
|
-
### Full Signature
|
|
227
|
-
|
|
228
|
-
```sql
|
|
229
|
-
SELECT dzql.register_entity(
|
|
230
|
-
p_table_name TEXT,
|
|
231
|
-
p_label_field TEXT,
|
|
232
|
-
p_searchable_fields TEXT[],
|
|
233
|
-
p_fk_includes JSONB DEFAULT '{}'::jsonb,
|
|
234
|
-
p_soft_delete BOOLEAN DEFAULT false,
|
|
235
|
-
p_temporal_fields JSONB DEFAULT '{}'::jsonb,
|
|
236
|
-
p_notification_paths JSONB DEFAULT '{}'::jsonb,
|
|
237
|
-
p_permission_paths JSONB DEFAULT '{}'::jsonb,
|
|
238
|
-
p_graph_rules JSONB DEFAULT '{}'::jsonb,
|
|
239
|
-
p_field_defaults JSONB DEFAULT '{}'::jsonb
|
|
240
|
-
);
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Parameters
|
|
244
|
-
|
|
245
|
-
| Parameter | Type | Required | Description |
|
|
246
|
-
|-----------|------|----------|-------------|
|
|
247
|
-
| `p_table_name` | TEXT | **yes** | Table name in database |
|
|
248
|
-
| `p_label_field` | TEXT | **yes** | Field used for LOOKUP display |
|
|
249
|
-
| `p_searchable_fields` | TEXT[] | **yes** | Fields searchable by SEARCH (min: 1) |
|
|
250
|
-
| `p_fk_includes` | JSONB | no | Foreign keys to dereference in GET |
|
|
251
|
-
| `p_soft_delete` | BOOLEAN | no | Enable soft delete (default: false) |
|
|
252
|
-
| `p_temporal_fields` | JSONB | no | Temporal field config (valid_from/valid_to) |
|
|
253
|
-
| `p_notification_paths` | JSONB | no | Who receives real-time updates |
|
|
254
|
-
| `p_permission_paths` | JSONB | no | CRUD permission rules |
|
|
255
|
-
| `p_graph_rules` | JSONB | no | Automatic relationship management, M2M, and primary_key |
|
|
256
|
-
| `p_field_defaults` | JSONB | no | Auto-populate fields on INSERT |
|
|
257
|
-
|
|
258
|
-
**Note:** `p_graph_rules` can include:
|
|
259
|
-
- `primary_key` - Array of column names for composite primary keys (default: `["id"]`)
|
|
260
|
-
- `many_to_many` - M2M relationship configurations
|
|
261
|
-
- `on_create`, `on_update`, `on_delete` - Graph rule triggers
|
|
262
|
-
|
|
263
|
-
### FK Includes
|
|
264
|
-
|
|
265
|
-
Configure which foreign keys to dereference in GET operations:
|
|
266
|
-
|
|
267
|
-
```sql
|
|
268
|
-
-- Single object dereference
|
|
269
|
-
'{"org": "organisations"}' -- venue.org_id -> full org object
|
|
270
|
-
|
|
271
|
-
-- Child array inclusion
|
|
272
|
-
'{"sites": "sites"}' -- Include all child sites (auto-detects FK)
|
|
273
|
-
|
|
274
|
-
-- Multiple
|
|
275
|
-
'{"org": "organisations", "sites": "sites", "venue": "venues"}'
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
**Result example:**
|
|
279
|
-
```javascript
|
|
280
|
-
{
|
|
281
|
-
id: 1,
|
|
282
|
-
name: "Madison Square Garden",
|
|
283
|
-
org_id: 3,
|
|
284
|
-
org: {id: 3, name: "Venue Management", ...}, // Dereferenced
|
|
285
|
-
sites: [ // Child array
|
|
286
|
-
{id: 1, name: "Main Entrance", ...},
|
|
287
|
-
{id: 2, name: "Concourse", ...}
|
|
288
|
-
]
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Temporal Fields
|
|
293
|
-
|
|
294
|
-
Enable temporal relationships with `valid_from`/`valid_to`:
|
|
295
|
-
|
|
296
|
-
```sql
|
|
297
|
-
'{
|
|
298
|
-
"valid_from": "valid_from", -- Column name for start date
|
|
299
|
-
"valid_to": "valid_to" -- Column name for end date
|
|
300
|
-
}'
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**Usage:**
|
|
304
|
-
```javascript
|
|
305
|
-
// Current relationships (default)
|
|
306
|
-
const rights = await ws.api.get.contractor_rights({id: 1});
|
|
307
|
-
|
|
308
|
-
// Historical relationships
|
|
309
|
-
const past = await ws.api.get.contractor_rights({id: 1, on_date: '2023-01-01'});
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### Field Defaults
|
|
313
|
-
|
|
314
|
-
Auto-populate fields on INSERT with values or variables:
|
|
315
|
-
|
|
316
|
-
```sql
|
|
317
|
-
'{
|
|
318
|
-
"owner_id": "@user_id", -- Current user ID
|
|
319
|
-
"created_by": "@user_id", -- Current user ID
|
|
320
|
-
"created_at": "@now", -- Current timestamp
|
|
321
|
-
"status": "draft" -- Literal value
|
|
322
|
-
}'
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**Available variables:**
|
|
326
|
-
- `@user_id` - Current user ID from `p_user_id`
|
|
327
|
-
- `@now` - Current timestamp
|
|
328
|
-
- `@today` - Current date
|
|
329
|
-
- Literal values - Any JSON value (`"draft"`, `0`, `true`)
|
|
330
|
-
|
|
331
|
-
**Behavior:**
|
|
332
|
-
- Only applied on INSERT (not UPDATE)
|
|
333
|
-
- Explicit values override defaults
|
|
334
|
-
- Reduces client boilerplate
|
|
335
|
-
|
|
336
|
-
See [Field Defaults Guide](../guides/field-defaults.md) for details.
|
|
337
|
-
|
|
338
|
-
### Composite Primary Keys
|
|
339
|
-
|
|
340
|
-
For tables with composite primary keys (not just `id`), specify the primary key columns via `graph_rules.primary_key`:
|
|
341
|
-
|
|
342
|
-
```sql
|
|
343
|
-
'{
|
|
344
|
-
"primary_key": ["template_id", "depends_on_template_id"]
|
|
345
|
-
}'
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
**Or in JavaScript:**
|
|
349
|
-
```javascript
|
|
350
|
-
{
|
|
351
|
-
tableName: "product_task_template_dependencies",
|
|
352
|
-
primaryKey: ["template_id", "depends_on_template_id"],
|
|
353
|
-
// ...
|
|
354
|
-
}
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
**Default:** `["id"]` - assumes a single `id` column as primary key.
|
|
358
|
-
|
|
359
|
-
**Why this matters:** The compiler generates event records with a `pk` field containing the primary key values. Without this configuration, tables with composite primary keys would fail with "record has no field id" errors.
|
|
360
|
-
|
|
361
|
-
**Generated SQL (composite):**
|
|
362
|
-
```sql
|
|
363
|
-
jsonb_build_object('template_id', v_result.template_id, 'depends_on_template_id', v_result.depends_on_template_id)
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
**Generated SQL (simple, default):**
|
|
367
|
-
```sql
|
|
368
|
-
jsonb_build_object('id', v_result.id)
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
**API Usage with Composite Keys:**
|
|
372
|
-
|
|
373
|
-
For entities with composite primary keys, the `get` and `delete` operations accept a JSONB object instead of an integer `id`:
|
|
374
|
-
|
|
375
|
-
```javascript
|
|
376
|
-
// Get by composite key
|
|
377
|
-
const dependency = await ws.api.get.product_task_template_dependencies({
|
|
378
|
-
template_id: 1,
|
|
379
|
-
depends_on_template_id: 2
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Delete by composite key
|
|
383
|
-
await ws.api.delete.product_task_template_dependencies({
|
|
384
|
-
template_id: 1,
|
|
385
|
-
depends_on_template_id: 2
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// Or using explicit pk object
|
|
389
|
-
await ws.api.delete.product_task_template_dependencies({
|
|
390
|
-
pk: { template_id: 1, depends_on_template_id: 2 }
|
|
391
|
-
});
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
**Note:** Entities with simple `id` primary keys continue to use `{id: 1}` for backwards compatibility.
|
|
395
|
-
|
|
396
|
-
### Many-to-Many Relationships
|
|
397
|
-
|
|
398
|
-
Configure M2M relationships via `graph_rules.many_to_many`:
|
|
399
|
-
|
|
400
|
-
```sql
|
|
401
|
-
'{
|
|
402
|
-
"many_to_many": {
|
|
403
|
-
"tags": {
|
|
404
|
-
"junction_table": "brand_tags",
|
|
405
|
-
"local_key": "brand_id",
|
|
406
|
-
"foreign_key": "tag_id",
|
|
407
|
-
"target_entity": "tags",
|
|
408
|
-
"id_field": "tag_ids",
|
|
409
|
-
"expand": false
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}'
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
**Client usage:**
|
|
416
|
-
```javascript
|
|
417
|
-
// Save with relationships in single call
|
|
418
|
-
await api.save_brands({
|
|
419
|
-
data: {
|
|
420
|
-
name: "My Brand",
|
|
421
|
-
tag_ids: [1, 2, 3] // Junction table synced atomically
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
// Response includes tag_ids array
|
|
426
|
-
{ id: 5, name: "My Brand", tag_ids: [1, 2, 3] }
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
**Configuration:**
|
|
430
|
-
- `junction_table` - Name of junction table
|
|
431
|
-
- `local_key` - FK to this entity
|
|
432
|
-
- `foreign_key` - FK to target entity
|
|
433
|
-
- `target_entity` - Target table name
|
|
434
|
-
- `id_field` - Field name for ID array
|
|
435
|
-
- `expand` - Include full objects (default: false)
|
|
436
|
-
|
|
437
|
-
See [Many-to-Many Guide](../guides/many-to-many.md) for details.
|
|
438
|
-
|
|
439
|
-
### Example Registration (Basic)
|
|
440
|
-
|
|
441
|
-
```sql
|
|
442
|
-
SELECT dzql.register_entity(
|
|
443
|
-
'venues', -- table name
|
|
444
|
-
'name', -- label field
|
|
445
|
-
array['name', 'address', 'description'], -- searchable
|
|
446
|
-
'{"org": "organisations", "sites": "sites"}', -- FK includes
|
|
447
|
-
false, -- soft delete
|
|
448
|
-
'{}', -- temporal (none)
|
|
449
|
-
'{ -- notifications
|
|
450
|
-
"ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"]
|
|
451
|
-
}',
|
|
452
|
-
'{ -- permissions
|
|
453
|
-
"create": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
454
|
-
"update": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
455
|
-
"delete": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
456
|
-
"view": []
|
|
457
|
-
}',
|
|
458
|
-
'{ -- graph rules
|
|
459
|
-
"on_create": {
|
|
460
|
-
"establish_site": {
|
|
461
|
-
"description": "Create default site",
|
|
462
|
-
"actions": [{
|
|
463
|
-
"type": "create",
|
|
464
|
-
"entity": "sites",
|
|
465
|
-
"data": {"name": "Main Site", "venue_id": "@id"}
|
|
466
|
-
}]
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}',
|
|
470
|
-
'{}' -- field defaults (none)
|
|
471
|
-
);
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### Example Registration (With All Features)
|
|
475
|
-
|
|
476
|
-
```sql
|
|
477
|
-
SELECT dzql.register_entity(
|
|
478
|
-
'resources',
|
|
479
|
-
'title',
|
|
480
|
-
ARRAY['title', 'description'],
|
|
481
|
-
'{"org": "organisations"}', -- FK includes
|
|
482
|
-
false, -- soft delete
|
|
483
|
-
'{}', -- temporal
|
|
484
|
-
'{}', -- notifications
|
|
485
|
-
'{ -- permissions
|
|
486
|
-
"view": [],
|
|
487
|
-
"create": [],
|
|
488
|
-
"update": ["@owner_id"],
|
|
489
|
-
"delete": ["@owner_id"]
|
|
490
|
-
}',
|
|
491
|
-
'{ -- graph rules
|
|
492
|
-
"many_to_many": {
|
|
493
|
-
"tags": {
|
|
494
|
-
"junction_table": "resource_tags",
|
|
495
|
-
"local_key": "resource_id",
|
|
496
|
-
"foreign_key": "tag_id",
|
|
497
|
-
"target_entity": "tags",
|
|
498
|
-
"id_field": "tag_ids",
|
|
499
|
-
"expand": false
|
|
500
|
-
},
|
|
501
|
-
"collaborators": {
|
|
502
|
-
"junction_table": "resource_collaborators",
|
|
503
|
-
"local_key": "resource_id",
|
|
504
|
-
"foreign_key": "user_id",
|
|
505
|
-
"target_entity": "users",
|
|
506
|
-
"id_field": "collaborator_ids",
|
|
507
|
-
"expand": true
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}',
|
|
511
|
-
'{ -- field defaults
|
|
512
|
-
"owner_id": "@user_id",
|
|
513
|
-
"created_by": "@user_id",
|
|
514
|
-
"created_at": "@now",
|
|
515
|
-
"status": "draft"
|
|
516
|
-
}'
|
|
517
|
-
);
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
**Client usage:**
|
|
521
|
-
```javascript
|
|
522
|
-
// Single call with all features!
|
|
523
|
-
const resource = await api.save_resources({
|
|
524
|
-
data: {
|
|
525
|
-
title: "My Resource",
|
|
526
|
-
tag_ids: [1, 2, 3],
|
|
527
|
-
collaborator_ids: [10, 20]
|
|
528
|
-
// owner_id, created_by, created_at, status auto-populated
|
|
529
|
-
}
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
// Response
|
|
533
|
-
{
|
|
534
|
-
id: 1,
|
|
535
|
-
title: "My Resource",
|
|
536
|
-
owner_id: 123, // From field defaults
|
|
537
|
-
created_by: 123, // From field defaults
|
|
538
|
-
created_at: "2025-11-20...", // From field defaults
|
|
539
|
-
status: "draft", // From field defaults
|
|
540
|
-
tag_ids: [1, 2, 3], // M2M IDs
|
|
541
|
-
collaborator_ids: [10, 20], // M2M IDs
|
|
542
|
-
collaborators: [...] // Full objects (expand: true)
|
|
543
|
-
}
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
---
|
|
547
|
-
|
|
548
|
-
## Search Operators
|
|
549
|
-
|
|
550
|
-
The SEARCH operation supports advanced filtering via the `filters` object.
|
|
551
|
-
|
|
552
|
-
### Operator Reference
|
|
553
|
-
|
|
554
|
-
| Operator | Syntax | Description | Example |
|
|
555
|
-
|----------|--------|-------------|---------|
|
|
556
|
-
| **Exact match** | `field: value` | Equality | `{name: 'Alice'}` |
|
|
557
|
-
| **Greater than** | `{gt: n}` | `>` | `{age: {gt: 18}}` |
|
|
558
|
-
| **Greater or equal** | `{gte: n}` | `>=` | `{age: {gte: 18}}` |
|
|
559
|
-
| **Less than** | `{lt: n}` | `<` | `{age: {lt: 65}}` |
|
|
560
|
-
| **Less or equal** | `{lte: n}` | `<=` | `{age: {lte: 65}}` |
|
|
561
|
-
| **Not equal** | `{neq: v}` | `!=` | `{status: {neq: 'deleted'}}` |
|
|
562
|
-
| **Between** | `{between: [a, b]}` | `BETWEEN a AND b` | `{age: {between: [18, 65]}}` |
|
|
563
|
-
| **LIKE** | `{like: 'pattern'}` | Case-sensitive pattern | `{name: {like: '%Garden%'}}` |
|
|
564
|
-
| **ILIKE** | `{ilike: 'pattern'}` | Case-insensitive pattern | `{name: {ilike: '%garden%'}}` |
|
|
565
|
-
| **IS NULL** | `field: null` | NULL check | `{description: null}` |
|
|
566
|
-
| **IS NOT NULL** | `{not_null: true}` | NOT NULL check | `{description: {not_null: true}}` |
|
|
567
|
-
| **IN array** | `field: [...]` | `IN (...)` | `{city: ['NYC', 'LA']}` |
|
|
568
|
-
| **NOT IN array** | `{not_in: [...]}` | `NOT IN (...)` | `{status: {not_in: ['deleted']}}` |
|
|
569
|
-
| **Text search** | `_search: 'terms'` | Across searchable fields | `{_search: 'madison garden'}` |
|
|
570
|
-
|
|
571
|
-
### Complete Example
|
|
572
|
-
|
|
573
|
-
```javascript
|
|
574
|
-
const results = await ws.api.search.venues({
|
|
575
|
-
filters: {
|
|
576
|
-
// Exact match
|
|
577
|
-
city: 'New York',
|
|
578
|
-
|
|
579
|
-
// Comparison
|
|
580
|
-
capacity: {gte: 1000, lt: 5000},
|
|
581
|
-
|
|
582
|
-
// Pattern matching
|
|
583
|
-
name: {ilike: '%garden%'},
|
|
584
|
-
|
|
585
|
-
// NULL checks
|
|
586
|
-
description: {not_null: true},
|
|
587
|
-
|
|
588
|
-
// Arrays
|
|
589
|
-
categories: ['sports', 'music'],
|
|
590
|
-
status: {not_in: ['deleted', 'closed']},
|
|
591
|
-
|
|
592
|
-
// Text search (across all searchable_fields)
|
|
593
|
-
_search: 'madison square'
|
|
594
|
-
},
|
|
595
|
-
sort: {field: 'capacity', order: 'desc'},
|
|
596
|
-
page: 1,
|
|
597
|
-
limit: 25
|
|
598
|
-
});
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
---
|
|
602
|
-
|
|
603
|
-
## Graph Rules
|
|
604
|
-
|
|
605
|
-
Automatically manage entity relationships when data changes.
|
|
606
|
-
|
|
607
|
-
### Structure
|
|
608
|
-
|
|
609
|
-
```jsonb
|
|
610
|
-
{
|
|
611
|
-
"on_create": {
|
|
612
|
-
"rule_name": {
|
|
613
|
-
"description": "Human-readable description",
|
|
614
|
-
"condition": "@after.field = 'value'", // Optional: only run if condition is true
|
|
615
|
-
"actions": [
|
|
616
|
-
{
|
|
617
|
-
"type": "create|update|delete|validate|execute",
|
|
618
|
-
"entity": "target_table", // for create/update/delete
|
|
619
|
-
"data": {"field": "@variable"}, // for create/update
|
|
620
|
-
"match": {"field": "@variable"}, // for update/delete
|
|
621
|
-
"function": "function_name", // for validate/execute
|
|
622
|
-
"params": {"param": "@variable"}, // for validate/execute
|
|
623
|
-
"error_message": "Validation failed" // for validate (optional)
|
|
624
|
-
}
|
|
625
|
-
]
|
|
626
|
-
}
|
|
627
|
-
},
|
|
628
|
-
"on_update": { /* same structure */ },
|
|
629
|
-
"on_delete": { /* same structure */ }
|
|
630
|
-
}
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
### Action Types
|
|
634
|
-
|
|
635
|
-
| Type | Fields | Description |
|
|
636
|
-
|------|--------|-------------|
|
|
637
|
-
| `create` | `entity`, `data` | INSERT new record |
|
|
638
|
-
| `update` | `entity`, `match`, `data` | UPDATE matching records |
|
|
639
|
-
| `delete` | `entity`, `match` | DELETE matching records |
|
|
640
|
-
| `validate` | `function`, `params`, `error_message` | Call validation function, rollback if returns false |
|
|
641
|
-
| `execute` | `function`, `params` | Fire-and-forget function execution |
|
|
642
|
-
|
|
643
|
-
### Variables
|
|
644
|
-
|
|
645
|
-
Variables reference data from the triggering operation:
|
|
646
|
-
|
|
647
|
-
| Variable | Description | Example |
|
|
648
|
-
|----------|-------------|---------|
|
|
649
|
-
| `@user_id` | Current authenticated user | `"created_by": "@user_id"` |
|
|
650
|
-
| `@id` | Primary key of the record | `"org_id": "@id"` |
|
|
651
|
-
| `@field_name` | Any field from the record | `"org_id": "@org_id"` |
|
|
652
|
-
| `@now` | Current timestamp | `"created_at": "@now"` |
|
|
653
|
-
| `@today` | Current date | `"valid_from": "@today"` |
|
|
654
|
-
|
|
655
|
-
### Common Patterns
|
|
656
|
-
|
|
657
|
-
#### Creator Becomes Owner
|
|
658
|
-
```jsonb
|
|
659
|
-
{
|
|
660
|
-
"on_create": {
|
|
661
|
-
"establish_ownership": {
|
|
662
|
-
"description": "Creator becomes member of organisation",
|
|
663
|
-
"actions": [{
|
|
664
|
-
"type": "create",
|
|
665
|
-
"entity": "acts_for",
|
|
666
|
-
"data": {
|
|
667
|
-
"user_id": "@user_id",
|
|
668
|
-
"org_id": "@id",
|
|
669
|
-
"valid_from": "@today"
|
|
670
|
-
}
|
|
671
|
-
}]
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
#### Cascade Delete
|
|
678
|
-
```jsonb
|
|
679
|
-
{
|
|
680
|
-
"on_delete": {
|
|
681
|
-
"cascade_venues": {
|
|
682
|
-
"description": "Delete all venues when org is deleted",
|
|
683
|
-
"actions": [{
|
|
684
|
-
"type": "delete",
|
|
685
|
-
"entity": "venues",
|
|
686
|
-
"match": {"org_id": "@id"}
|
|
687
|
-
}]
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
#### Temporal Transition
|
|
694
|
-
```jsonb
|
|
695
|
-
{
|
|
696
|
-
"on_create": {
|
|
697
|
-
"expire_previous": {
|
|
698
|
-
"description": "End previous temporal relationship",
|
|
699
|
-
"actions": [{
|
|
700
|
-
"type": "update",
|
|
701
|
-
"entity": "acts_for",
|
|
702
|
-
"match": {
|
|
703
|
-
"user_id": "@user_id",
|
|
704
|
-
"org_id": "@org_id",
|
|
705
|
-
"valid_to": null
|
|
706
|
-
},
|
|
707
|
-
"data": {
|
|
708
|
-
"valid_to": "@valid_from"
|
|
709
|
-
}
|
|
710
|
-
}]
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
#### Data Validation
|
|
717
|
-
```jsonb
|
|
718
|
-
{
|
|
719
|
-
"on_create": {
|
|
720
|
-
"validate_positive_price": {
|
|
721
|
-
"description": "Ensure price is positive",
|
|
722
|
-
"actions": [{
|
|
723
|
-
"type": "validate",
|
|
724
|
-
"function": "validate_positive_value",
|
|
725
|
-
"params": {"p_value": "@price"},
|
|
726
|
-
"error_message": "Price must be positive"
|
|
727
|
-
}]
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
**Note:** Validation function must return BOOLEAN:
|
|
734
|
-
```sql
|
|
735
|
-
CREATE FUNCTION validate_positive_value(p_value INT)
|
|
736
|
-
RETURNS BOOLEAN AS $$
|
|
737
|
-
SELECT p_value > 0;
|
|
738
|
-
$$ LANGUAGE sql;
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
#### Conditional Execution
|
|
742
|
-
```jsonb
|
|
743
|
-
{
|
|
744
|
-
"on_update": {
|
|
745
|
-
"prevent_posted_changes": {
|
|
746
|
-
"description": "Prevent modification of posted records",
|
|
747
|
-
"condition": "@before.status = 'posted'",
|
|
748
|
-
"actions": [{
|
|
749
|
-
"type": "validate",
|
|
750
|
-
"function": "always_false",
|
|
751
|
-
"params": {},
|
|
752
|
-
"error_message": "Cannot modify posted records"
|
|
753
|
-
}]
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
**Available in conditions:** `@before.field`, `@after.field`, `@user_id`, and SQL expressions.
|
|
760
|
-
|
|
761
|
-
#### Fire-and-Forget Actions
|
|
762
|
-
```jsonb
|
|
763
|
-
{
|
|
764
|
-
"on_create": {
|
|
765
|
-
"send_notification": {
|
|
766
|
-
"description": "Notify external system",
|
|
767
|
-
"actions": [{
|
|
768
|
-
"type": "execute",
|
|
769
|
-
"function": "log_event",
|
|
770
|
-
"params": {"p_event": "New record created", "p_record_id": "@id"}
|
|
771
|
-
}]
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
**Note:** Execute actions don't affect transaction. Function errors are logged but don't rollback.
|
|
778
|
-
|
|
779
|
-
### Execution
|
|
780
|
-
|
|
781
|
-
- **Atomic**: All rules execute in the same transaction
|
|
782
|
-
- **Sequential**: Actions execute in order within each rule
|
|
783
|
-
- **Rollback**: If any action fails, entire transaction rolls back
|
|
784
|
-
- **Events**: Each action generates its own audit event
|
|
785
|
-
|
|
786
|
-
---
|
|
787
|
-
|
|
788
|
-
## Permission & Notification Paths
|
|
789
|
-
|
|
790
|
-
Paths use a unified syntax for both permissions and notifications.
|
|
791
|
-
|
|
792
|
-
### Path Syntax
|
|
793
|
-
|
|
794
|
-
```
|
|
795
|
-
@field->table[filter]{temporal}.target_field
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
**Components:**
|
|
799
|
-
- `@field` - Start from a field in the current record
|
|
800
|
-
- `->table` - Navigate to related table
|
|
801
|
-
- `[filter]` - WHERE clause (`$` = current field value)
|
|
802
|
-
- `{temporal}` - Apply temporal filtering (`{active}` = valid now)
|
|
803
|
-
- `.target_field` - Extract this field as result
|
|
804
|
-
|
|
805
|
-
### Permission Paths
|
|
806
|
-
|
|
807
|
-
Control who can perform CRUD operations:
|
|
808
|
-
|
|
809
|
-
```sql
|
|
810
|
-
'{
|
|
811
|
-
"create": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
812
|
-
"update": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
813
|
-
"delete": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
814
|
-
"view": [] -- Empty array = public access
|
|
815
|
-
}'
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
**Permission types:**
|
|
819
|
-
- `create` - Who can create records
|
|
820
|
-
- `update` - Who can modify records
|
|
821
|
-
- `delete` - Who can remove records
|
|
822
|
-
- `view` - Who can read records (empty = public)
|
|
823
|
-
|
|
824
|
-
**Behavior:**
|
|
825
|
-
- User's `user_id` must be in resolved set of user_ids
|
|
826
|
-
- Checked before operation executes
|
|
827
|
-
- Empty array = allow all
|
|
828
|
-
- Missing permission type = deny all
|
|
829
|
-
|
|
830
|
-
### Notification Paths
|
|
831
|
-
|
|
832
|
-
Determine who receives real-time updates:
|
|
833
|
-
|
|
834
|
-
```sql
|
|
835
|
-
'{
|
|
836
|
-
"ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
837
|
-
"sponsorship": ["@sponsor_org_id->acts_for[org_id=$]{active}.user_id"]
|
|
838
|
-
}'
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
**Behavior:**
|
|
842
|
-
- Resolves to array of user_ids or `null`
|
|
843
|
-
- `null` = broadcast to all authenticated users
|
|
844
|
-
- Array = send only to specified users
|
|
845
|
-
- Multiple paths = union of all resolved user_ids
|
|
846
|
-
|
|
847
|
-
### Path Examples
|
|
848
|
-
|
|
849
|
-
```sql
|
|
850
|
-
-- Direct user reference
|
|
851
|
-
'@user_id'
|
|
852
|
-
|
|
853
|
-
-- Via organization
|
|
854
|
-
'@org_id->acts_for[org_id=$]{active}.user_id'
|
|
855
|
-
|
|
856
|
-
-- Via nested relationship
|
|
857
|
-
'@venue_id->venues.org_id->acts_for[org_id=$]{active}.user_id'
|
|
858
|
-
|
|
859
|
-
-- Via multiple relationships
|
|
860
|
-
'@package_id->packages.owner_org_id->acts_for[org_id=$]{active}.user_id'
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
---
|
|
864
|
-
|
|
865
|
-
## Custom Functions
|
|
866
|
-
|
|
867
|
-
Extend DZQL with custom PostgreSQL or Bun functions.
|
|
868
|
-
|
|
869
|
-
### PostgreSQL Functions
|
|
870
|
-
|
|
871
|
-
Create stored procedures and call via proxy API:
|
|
872
|
-
|
|
873
|
-
```sql
|
|
874
|
-
CREATE OR REPLACE FUNCTION my_function(
|
|
875
|
-
p_user_id INT, -- REQUIRED: First parameter
|
|
876
|
-
p_param TEXT DEFAULT 'default'
|
|
877
|
-
) RETURNS JSONB
|
|
878
|
-
LANGUAGE plpgsql
|
|
879
|
-
SECURITY DEFINER
|
|
880
|
-
AS $$
|
|
881
|
-
BEGIN
|
|
882
|
-
-- Your logic here
|
|
883
|
-
RETURN jsonb_build_object('result', p_param);
|
|
884
|
-
END;
|
|
885
|
-
$$;
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
**Call:**
|
|
889
|
-
```javascript
|
|
890
|
-
const result = await ws.api.my_function({param: 'value'});
|
|
891
|
-
```
|
|
892
|
-
|
|
893
|
-
**Conventions:**
|
|
894
|
-
- First parameter **must** be `p_user_id INT`
|
|
895
|
-
- Can access full PostgreSQL ecosystem
|
|
896
|
-
- Automatically transactional
|
|
897
|
-
- Optional registration in `dzql.registry`
|
|
898
|
-
|
|
899
|
-
### Bun Functions
|
|
900
|
-
|
|
901
|
-
Create JavaScript functions in server:
|
|
902
|
-
|
|
903
|
-
```javascript
|
|
904
|
-
// server/api.js
|
|
905
|
-
export async function myBunFunction(userId, params = {}) {
|
|
906
|
-
const { param = 'default' } = params;
|
|
907
|
-
|
|
908
|
-
// Can use db.api for database access
|
|
909
|
-
// const data = await db.api.get.users({id: userId}, userId);
|
|
910
|
-
|
|
911
|
-
return { result: param };
|
|
912
|
-
}
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
**Server setup:**
|
|
916
|
-
```javascript
|
|
917
|
-
import * as customApi from './server/api.js';
|
|
918
|
-
const server = createServer({ customApi });
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
**Call:**
|
|
922
|
-
```javascript
|
|
923
|
-
const result = await ws.api.myBunFunction({param: 'value'});
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
**Conventions:**
|
|
927
|
-
- First parameter is `userId` (number)
|
|
928
|
-
- Second parameter is `params` object
|
|
929
|
-
- Can access `db.api.*` operations
|
|
930
|
-
- Can use any npm packages
|
|
931
|
-
- Return JSON-serializable data
|
|
932
|
-
|
|
933
|
-
### Function Comparison
|
|
934
|
-
|
|
935
|
-
| Feature | PostgreSQL | Bun |
|
|
936
|
-
|---------|-----------|-----|
|
|
937
|
-
| **Language** | SQL/PL/pgSQL | JavaScript |
|
|
938
|
-
| **Access to** | Database only | Database + npm ecosystem |
|
|
939
|
-
| **Transaction** | Automatic | Manual (via db.api) |
|
|
940
|
-
| **Performance** | Faster (no network) | Slower (WebSocket overhead) |
|
|
941
|
-
| **Use case** | Data-heavy operations | Complex business logic |
|
|
942
|
-
|
|
943
|
-
---
|
|
944
|
-
|
|
945
|
-
## Authentication
|
|
946
|
-
|
|
947
|
-
JWT-based authentication with automatic user_id injection.
|
|
948
|
-
|
|
949
|
-
### Register User
|
|
950
|
-
|
|
951
|
-
```javascript
|
|
952
|
-
const result = await ws.api.register_user({
|
|
953
|
-
email: 'user@example.com',
|
|
954
|
-
password: 'secure-password'
|
|
955
|
-
});
|
|
956
|
-
```
|
|
957
|
-
|
|
958
|
-
**With options (for extended registration):**
|
|
959
|
-
```javascript
|
|
960
|
-
const result = await ws.api.register_user({
|
|
961
|
-
email: 'user@example.com',
|
|
962
|
-
password: 'secure-password',
|
|
963
|
-
options: {
|
|
964
|
-
org_name: 'Acme Corp',
|
|
965
|
-
role: 'admin'
|
|
966
|
-
}
|
|
967
|
-
});
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
**Parameters:**
|
|
971
|
-
| Field | Type | Required | Description |
|
|
972
|
-
|-------|------|----------|-------------|
|
|
973
|
-
| `email` | string | yes | User email address |
|
|
974
|
-
| `password` | string | yes | User password |
|
|
975
|
-
| `options` | object | no | Additional JSONB data passed to `register_user()` function |
|
|
976
|
-
|
|
977
|
-
**Returns:**
|
|
978
|
-
```javascript
|
|
979
|
-
{
|
|
980
|
-
user_id: 1,
|
|
981
|
-
email: 'user@example.com',
|
|
982
|
-
token: 'eyJ...',
|
|
983
|
-
profile: {...}
|
|
984
|
-
}
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
**Note:** The `options` parameter requires your `register_user` PostgreSQL function to accept a third parameter:
|
|
988
|
-
```sql
|
|
989
|
-
CREATE OR REPLACE FUNCTION register_user(
|
|
990
|
-
p_email TEXT,
|
|
991
|
-
p_password TEXT,
|
|
992
|
-
p_options JSONB DEFAULT NULL
|
|
993
|
-
) RETURNS JSONB AS $$
|
|
994
|
-
-- Access options: p_options->>'org_name'
|
|
995
|
-
$$;
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
### Login
|
|
999
|
-
|
|
1000
|
-
```javascript
|
|
1001
|
-
const result = await ws.api.login_user({
|
|
1002
|
-
email: 'user@example.com',
|
|
1003
|
-
password: 'password'
|
|
1004
|
-
});
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
**With options:**
|
|
1008
|
-
```javascript
|
|
1009
|
-
const result = await ws.api.login_user({
|
|
1010
|
-
email: 'user@example.com',
|
|
1011
|
-
password: 'password',
|
|
1012
|
-
options: {
|
|
1013
|
-
device_id: 'abc123',
|
|
1014
|
-
remember_me: true
|
|
1015
|
-
}
|
|
1016
|
-
});
|
|
1017
|
-
```
|
|
1018
|
-
|
|
1019
|
-
**Parameters:**
|
|
1020
|
-
| Field | Type | Required | Description |
|
|
1021
|
-
|-------|------|----------|-------------|
|
|
1022
|
-
| `email` | string | yes | User email address |
|
|
1023
|
-
| `password` | string | yes | User password |
|
|
1024
|
-
| `options` | object | no | Additional JSONB data passed to `login_user()` function |
|
|
1025
|
-
|
|
1026
|
-
**Returns:** Same as register
|
|
1027
|
-
|
|
1028
|
-
**Note:** The `options` parameter requires your `login_user` PostgreSQL function to accept a third parameter:
|
|
1029
|
-
```sql
|
|
1030
|
-
CREATE OR REPLACE FUNCTION login_user(
|
|
1031
|
-
p_email TEXT,
|
|
1032
|
-
p_password TEXT,
|
|
1033
|
-
p_options JSONB DEFAULT NULL
|
|
1034
|
-
) RETURNS JSONB AS $$
|
|
1035
|
-
-- Access options: p_options->>'device_id'
|
|
1036
|
-
$$;
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
### Logout
|
|
1040
|
-
|
|
1041
|
-
```javascript
|
|
1042
|
-
await ws.api.logout();
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
### Token Storage
|
|
1046
|
-
|
|
1047
|
-
```javascript
|
|
1048
|
-
// Save token
|
|
1049
|
-
localStorage.setItem('dzql_token', result.token);
|
|
1050
|
-
|
|
1051
|
-
// Auto-connect with token
|
|
1052
|
-
const ws = new WebSocketManager();
|
|
1053
|
-
await ws.connect(); // Automatically uses token from localStorage
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
### User ID Injection
|
|
1057
|
-
|
|
1058
|
-
- **Client**: `user_id` automatically injected from JWT
|
|
1059
|
-
- **Server**: `user_id` must be passed explicitly as second parameter
|
|
1060
|
-
|
|
1061
|
-
```javascript
|
|
1062
|
-
// Client
|
|
1063
|
-
const user = await ws.api.get.users({id: 1}); // userId auto-injected
|
|
1064
|
-
|
|
1065
|
-
// Server
|
|
1066
|
-
const user = await db.api.get.users({id: 1}, userId); // userId explicit
|
|
1067
|
-
```
|
|
1068
|
-
|
|
1069
|
-
---
|
|
1070
|
-
|
|
1071
|
-
## Real-time Events
|
|
1072
|
-
|
|
1073
|
-
All database changes trigger WebSocket events.
|
|
1074
|
-
|
|
1075
|
-
### Event Flow
|
|
1076
|
-
|
|
1077
|
-
1. Database trigger fires on INSERT/UPDATE/DELETE
|
|
1078
|
-
2. Notification paths resolve affected user_ids
|
|
1079
|
-
3. Event written to `dzql.events` table
|
|
1080
|
-
4. PostgreSQL NOTIFY on 'dzql' channel
|
|
1081
|
-
5. Bun server filters by `notify_users`
|
|
1082
|
-
6. WebSocket message sent to affected clients
|
|
1083
|
-
|
|
1084
|
-
### Listening for Events
|
|
1085
|
-
|
|
1086
|
-
```javascript
|
|
1087
|
-
const unsubscribe = ws.onBroadcast((method, params) => {
|
|
1088
|
-
console.log(`Event: ${method}`, params);
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
// Stop listening
|
|
1092
|
-
unsubscribe();
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
### Event Format
|
|
1096
|
-
|
|
1097
|
-
**Method:** `"{table}:{operation}"`
|
|
1098
|
-
- Examples: `"users:insert"`, `"venues:update"`, `"sites:delete"`
|
|
1099
|
-
|
|
1100
|
-
**Params:**
|
|
1101
|
-
```javascript
|
|
1102
|
-
{
|
|
1103
|
-
table: 'venues',
|
|
1104
|
-
op: 'insert' | 'update' | 'delete',
|
|
1105
|
-
pk: {id: 1}, // Primary key
|
|
1106
|
-
data: {...}, // Record data (new state for insert/update, null for delete)
|
|
1107
|
-
user_id: 123, // Who made the change
|
|
1108
|
-
at: '2025-01-01T...' // Timestamp
|
|
1109
|
-
}
|
|
1110
|
-
```
|
|
1111
|
-
|
|
1112
|
-
**Event data by operation:**
|
|
1113
|
-
| Operation | `data` field contains |
|
|
1114
|
-
|-----------|----------------------|
|
|
1115
|
-
| `insert` | Full new record |
|
|
1116
|
-
| `update` | Full updated record (new state only) |
|
|
1117
|
-
| `delete` | `null` |
|
|
1118
|
-
|
|
1119
|
-
**Note:** The `notify_users` field is used internally for routing but is stripped from the broadcast message sent to clients.
|
|
1120
|
-
|
|
1121
|
-
### Event Handling Pattern
|
|
1122
|
-
|
|
1123
|
-
```javascript
|
|
1124
|
-
ws.onBroadcast((method, params) => {
|
|
1125
|
-
const { table, op, pk, data } = params;
|
|
1126
|
-
|
|
1127
|
-
if (method === 'todos:insert') {
|
|
1128
|
-
state.todos.push(data);
|
|
1129
|
-
} else if (method === 'todos:update') {
|
|
1130
|
-
const idx = state.todos.findIndex(t => t.id === pk.id);
|
|
1131
|
-
if (idx !== -1) state.todos[idx] = data;
|
|
1132
|
-
} else if (method === 'todos:delete') {
|
|
1133
|
-
state.todos = state.todos.filter(t => t.id !== pk.id);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
render();
|
|
1137
|
-
});
|
|
1138
|
-
```
|
|
1139
|
-
|
|
1140
|
-
---
|
|
1141
|
-
|
|
1142
|
-
## Live Query Subscriptions
|
|
1143
|
-
|
|
1144
|
-
Subscribe to denormalized documents and receive automatic updates when underlying data changes. Subscriptions use a PostgreSQL-first architecture where all change detection happens in the database.
|
|
1145
|
-
|
|
1146
|
-
For complete documentation, see **[Live Query Subscriptions Guide](../guides/subscriptions.md)** and **[Quick Start](../getting-started/subscriptions-quick-start.md)**.
|
|
1147
|
-
|
|
1148
|
-
### Quick Example
|
|
1149
|
-
|
|
1150
|
-
```javascript
|
|
1151
|
-
// Subscribe to venue with all related data
|
|
1152
|
-
const { data, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
1153
|
-
{ venue_id: 123 },
|
|
1154
|
-
(updatedVenue) => {
|
|
1155
|
-
// Called automatically when venue, org, or sites change
|
|
1156
|
-
console.log('Updated:', updatedVenue);
|
|
1157
|
-
// updatedVenue = { id: 123, name: '...', org: {...}, sites: [...] }
|
|
1158
|
-
}
|
|
1159
|
-
);
|
|
1160
|
-
|
|
1161
|
-
// Initial data available immediately
|
|
1162
|
-
console.log('Initial:', data);
|
|
1163
|
-
|
|
1164
|
-
// Later: cleanup
|
|
1165
|
-
await unsubscribe();
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
### Creating a Subscribable
|
|
1169
|
-
|
|
1170
|
-
Define subscribables in SQL:
|
|
1171
|
-
|
|
1172
|
-
```sql
|
|
1173
|
-
SELECT dzql.register_subscribable(
|
|
1174
|
-
'venue_detail', -- Name
|
|
1175
|
-
'{"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb, -- Permissions
|
|
1176
|
-
'{"venue_id": "int"}'::jsonb, -- Parameters
|
|
1177
|
-
'venues', -- Root table
|
|
1178
|
-
'{
|
|
1179
|
-
"org": "organisations",
|
|
1180
|
-
"sites": {"entity": "sites", "filter": "venue_id=$venue_id"}
|
|
1181
|
-
}'::jsonb -- Relations
|
|
1182
|
-
);
|
|
1183
|
-
```
|
|
1184
|
-
|
|
1185
|
-
### Compile and Deploy
|
|
1186
|
-
|
|
1187
|
-
```bash
|
|
1188
|
-
# Compile subscribable to PostgreSQL functions
|
|
1189
|
-
bun packages/dzql/src/compiler/cli/compile-subscribable.js venue.sql | psql $DATABASE_URL
|
|
1190
|
-
```
|
|
1191
|
-
|
|
1192
|
-
This generates three functions:
|
|
1193
|
-
- `venue_detail_can_subscribe(user_id, params)` - Permission check
|
|
1194
|
-
- `get_venue_detail(params, user_id)` - Query builder
|
|
1195
|
-
- `venue_detail_affected_documents(table, op, old, new)` - Change detector
|
|
1196
|
-
|
|
1197
|
-
### Subscription Lifecycle
|
|
1198
|
-
|
|
1199
|
-
1. **Subscribe**: Client calls `ws.api.subscribe_<name>(params, callback)`
|
|
1200
|
-
2. **Permission Check**: `<name>_can_subscribe()` validates access
|
|
1201
|
-
3. **Initial Query**: `get_<name>()` returns denormalized document
|
|
1202
|
-
4. **Register**: Server stores subscription in-memory
|
|
1203
|
-
5. **Database Change**: Any relevant table modification
|
|
1204
|
-
6. **Detect**: `<name>_affected_documents()` identifies affected subscriptions
|
|
1205
|
-
7. **Re-query**: `get_<name>()` fetches fresh data
|
|
1206
|
-
8. **Update**: Callback invoked with new data
|
|
1207
|
-
|
|
1208
|
-
### Unsubscribe
|
|
1209
|
-
|
|
1210
|
-
```javascript
|
|
1211
|
-
// Method 1: Use returned unsubscribe function
|
|
1212
|
-
const { unsubscribe } = await ws.api.subscribe_venue_detail(...);
|
|
1213
|
-
await unsubscribe();
|
|
1214
|
-
|
|
1215
|
-
// Method 2: Direct unsubscribe call
|
|
1216
|
-
await ws.api.unsubscribe_venue_detail({ venue_id: 123 });
|
|
1217
|
-
```
|
|
1218
|
-
|
|
1219
|
-
### Architecture Benefits
|
|
1220
|
-
|
|
1221
|
-
- **PostgreSQL-First**: All logic executes in database, not application code
|
|
1222
|
-
- **Zero Configuration**: Pattern matching on method names - no server changes needed
|
|
1223
|
-
- **Type Safe**: Compiled functions validated at deploy time
|
|
1224
|
-
- **Efficient**: In-memory registry, PostgreSQL does matching
|
|
1225
|
-
- **Secure**: Permission paths enforced at database level
|
|
1226
|
-
- **Scalable**: Stateless server, can add instances freely
|
|
1227
|
-
|
|
1228
|
-
### Common Patterns
|
|
1229
|
-
|
|
1230
|
-
**Single Table:**
|
|
1231
|
-
```sql
|
|
1232
|
-
SELECT dzql.register_subscribable(
|
|
1233
|
-
'user_settings',
|
|
1234
|
-
'{"subscribe": ["@user_id"]}'::jsonb,
|
|
1235
|
-
'{"user_id": "int"}'::jsonb,
|
|
1236
|
-
'user_settings',
|
|
1237
|
-
'{}'::jsonb
|
|
1238
|
-
);
|
|
1239
|
-
```
|
|
1240
|
-
|
|
1241
|
-
**With Relations:**
|
|
1242
|
-
```sql
|
|
1243
|
-
SELECT dzql.register_subscribable(
|
|
1244
|
-
'booking_detail',
|
|
1245
|
-
'{"subscribe": ["@user_id"]}'::jsonb,
|
|
1246
|
-
'{"booking_id": "int"}'::jsonb,
|
|
1247
|
-
'bookings',
|
|
1248
|
-
'{
|
|
1249
|
-
"venue": "venues",
|
|
1250
|
-
"customer": "users",
|
|
1251
|
-
"items": {"entity": "booking_items", "filter": "booking_id=$booking_id"}
|
|
1252
|
-
}'::jsonb
|
|
1253
|
-
);
|
|
1254
|
-
```
|
|
1255
|
-
|
|
1256
|
-
**Multiple Permission Paths (OR logic):**
|
|
1257
|
-
```sql
|
|
1258
|
-
SELECT dzql.register_subscribable(
|
|
1259
|
-
'venue_admin',
|
|
1260
|
-
'{
|
|
1261
|
-
"subscribe": [
|
|
1262
|
-
"@owner_id",
|
|
1263
|
-
"@org_id->acts_for[org_id=$]{active}.user_id"
|
|
1264
|
-
]
|
|
1265
|
-
}'::jsonb,
|
|
1266
|
-
'{"venue_id": "int"}'::jsonb,
|
|
1267
|
-
'venues',
|
|
1268
|
-
'{"sites": {"entity": "sites", "filter": "venue_id=$venue_id"}}'::jsonb
|
|
1269
|
-
);
|
|
1270
|
-
```
|
|
1271
|
-
|
|
1272
|
-
### See Also
|
|
1273
|
-
|
|
1274
|
-
- **[Live Query Subscriptions Guide](../guides/subscriptions.md)** - Complete reference
|
|
1275
|
-
- **[Quick Start Guide](../getting-started/subscriptions-quick-start.md)** - 5-minute tutorial
|
|
1276
|
-
- **[Permission Paths](#permission--notification-paths)** - Path DSL syntax
|
|
1277
|
-
|
|
1278
|
-
---
|
|
1279
|
-
|
|
1280
|
-
## Temporal Relationships
|
|
1281
|
-
|
|
1282
|
-
Handle time-based relationships with `valid_from`/`valid_to` fields.
|
|
1283
|
-
|
|
1284
|
-
### Configuration
|
|
1285
|
-
|
|
1286
|
-
```sql
|
|
1287
|
-
SELECT dzql.register_entity(
|
|
1288
|
-
'contractor_rights',
|
|
1289
|
-
'contractor_name',
|
|
1290
|
-
array['contractor_name'],
|
|
1291
|
-
'{"contractor_org": "organisations", "venue": "venues"}',
|
|
1292
|
-
false,
|
|
1293
|
-
'{
|
|
1294
|
-
"valid_from": "valid_from",
|
|
1295
|
-
"valid_to": "valid_to"
|
|
1296
|
-
}',
|
|
1297
|
-
'{}', '{}'
|
|
1298
|
-
);
|
|
1299
|
-
```
|
|
1300
|
-
|
|
1301
|
-
### Usage
|
|
1302
|
-
|
|
1303
|
-
```javascript
|
|
1304
|
-
// Get current relationships (default)
|
|
1305
|
-
const rights = await ws.api.get.contractor_rights({id: 1});
|
|
1306
|
-
|
|
1307
|
-
// Get historical relationships
|
|
1308
|
-
const past = await ws.api.get.contractor_rights({
|
|
1309
|
-
id: 1,
|
|
1310
|
-
on_date: '2023-01-01'
|
|
1311
|
-
});
|
|
1312
|
-
```
|
|
1313
|
-
|
|
1314
|
-
### Path Syntax with Temporal
|
|
1315
|
-
|
|
1316
|
-
```sql
|
|
1317
|
-
-- Current relationships only
|
|
1318
|
-
'@org_id->acts_for[org_id=$]{active}.user_id'
|
|
1319
|
-
|
|
1320
|
-
-- All relationships (past and present)
|
|
1321
|
-
'@org_id->acts_for[org_id=$].user_id'
|
|
1322
|
-
```
|
|
1323
|
-
|
|
1324
|
-
---
|
|
1325
|
-
|
|
1326
|
-
## Error Messages
|
|
1327
|
-
|
|
1328
|
-
Common error messages and their meanings:
|
|
1329
|
-
|
|
1330
|
-
| Error | Cause | Solution |
|
|
1331
|
-
|-------|-------|----------|
|
|
1332
|
-
| `"record not found"` | GET on non-existent ID | Check ID exists, handle 404 |
|
|
1333
|
-
| `"Permission denied: view on users"` | User not in permission path | Check permissions, authenticate |
|
|
1334
|
-
| `"entity users not configured"` | Entity not registered | Call `dzql.register_entity()` |
|
|
1335
|
-
| `"Column foo does not exist in table users"` | Invalid filter field | Check searchable_fields config |
|
|
1336
|
-
| `"Invalid function name: foo"` | Function doesn't exist | Create function or check spelling |
|
|
1337
|
-
| `"Function not found"` | Custom function not registered | Export from api.js or create SQL function |
|
|
1338
|
-
| `"Authentication required"` | Not logged in | Call `login_user()` first |
|
|
1339
|
-
| `"Invalid token"` | Expired/invalid JWT | Re-authenticate |
|
|
1340
|
-
|
|
1341
|
-
---
|
|
1342
|
-
|
|
1343
|
-
## Server-Side API
|
|
1344
|
-
|
|
1345
|
-
For backend/Bun scripts, use `db.api`:
|
|
1346
|
-
|
|
1347
|
-
```javascript
|
|
1348
|
-
import { db, sql } from 'dzql';
|
|
1349
|
-
|
|
1350
|
-
// Direct SQL queries
|
|
1351
|
-
const users = await sql`SELECT * FROM users WHERE active = true`;
|
|
1352
|
-
|
|
1353
|
-
// DZQL operations (require explicit userId)
|
|
1354
|
-
const user = await db.api.get.users({id: 1}, userId);
|
|
1355
|
-
const saved = await db.api.save.users({name: 'John'}, userId);
|
|
1356
|
-
const results = await db.api.search.users({filters: {}}, userId);
|
|
1357
|
-
const deleted = await db.api.delete.users({id: 1}, userId);
|
|
1358
|
-
const options = await db.api.lookup.users({p_filter: 'jo'}, userId);
|
|
1359
|
-
|
|
1360
|
-
// Custom functions
|
|
1361
|
-
const result = await db.api.myCustomFunction({param: 'value'}, userId);
|
|
1362
|
-
```
|
|
1363
|
-
|
|
1364
|
-
**Key difference:** Server-side requires explicit `userId` as second parameter; client-side auto-injects from JWT.
|
|
1365
|
-
|
|
1366
|
-
---
|
|
1367
|
-
|
|
1368
|
-
## See Also
|
|
1369
|
-
|
|
1370
|
-
- [Getting Started Tutorial](../getting-started/tutorial.md) - Hands-on tutorial
|
|
1371
|
-
- [Claude Guide](../for-ai/claude-guide.md) - AI development guide
|
|
1372
|
-
- [Project README](../../../../README.md) - Project overview
|
|
1373
|
-
- [Venues Example](../../../venues/) - Complete working application
|