dzql 0.1.3 → 0.1.4
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/README.md +21 -6
- package/package.json +4 -3
- package/src/compiler/cli/index.js +174 -0
- package/src/compiler/codegen/graph-rules-codegen.js +259 -0
- package/src/compiler/codegen/notification-codegen.js +232 -0
- package/src/compiler/codegen/operation-codegen.js +555 -0
- package/src/compiler/codegen/permission-codegen.js +310 -0
- package/src/compiler/compiler.js +228 -0
- package/src/compiler/index.js +11 -0
- package/src/compiler/parser/entity-parser.js +299 -0
- package/src/compiler/parser/path-parser.js +290 -0
- package/src/database/migrations/002_functions.sql +39 -2
- package/src/database/migrations/003_operations.sql +10 -0
- package/src/database/migrations/005_entities.sql +112 -0
- package/GETTING_STARTED.md +0 -1104
- package/REFERENCE.md +0 -960
package/REFERENCE.md
DELETED
|
@@ -1,960 +0,0 @@
|
|
|
1
|
-
# DZQL API Reference
|
|
2
|
-
|
|
3
|
-
Complete API documentation for DZQL framework. For tutorials, see [GETTING_STARTED.md](GETTING_STARTED.md). For AI development guide, see [CLAUDE.md](../../docs/CLAUDE.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
|
-
- [Temporal Relationships](#temporal-relationships)
|
|
16
|
-
- [Error Messages](#error-messages)
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## The 5 Operations
|
|
21
|
-
|
|
22
|
-
Every registered entity automatically gets these 5 operations via the proxy API:
|
|
23
|
-
|
|
24
|
-
### GET - Retrieve Single Record
|
|
25
|
-
|
|
26
|
-
Fetch a single record by primary key with foreign keys dereferenced.
|
|
27
|
-
|
|
28
|
-
**Client:**
|
|
29
|
-
```javascript
|
|
30
|
-
const record = await ws.api.get.{entity}({id: 1});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
**Server:**
|
|
34
|
-
```javascript
|
|
35
|
-
const record = await db.api.get.{entity}({id: 1}, userId);
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**Parameters:**
|
|
39
|
-
| Field | Type | Required | Description |
|
|
40
|
-
|-------|------|----------|-------------|
|
|
41
|
-
| `id` | any | yes | Primary key value |
|
|
42
|
-
| `on_date` | string | no | Temporal filtering (ISO 8601 date) |
|
|
43
|
-
|
|
44
|
-
**Returns:** Object with all fields + dereferenced FKs
|
|
45
|
-
|
|
46
|
-
**Throws:** `"record not found"` if not exists
|
|
47
|
-
|
|
48
|
-
**Example:**
|
|
49
|
-
```javascript
|
|
50
|
-
const venue = await ws.api.get.venues({id: 1});
|
|
51
|
-
// {id: 1, name: "MSG", org: {id: 3, name: "Org"}, sites: [...]}
|
|
52
|
-
|
|
53
|
-
// With temporal filtering
|
|
54
|
-
const historical = await ws.api.get.venues({id: 1, on_date: '2023-01-01'});
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
### SAVE - Create or Update (Upsert)
|
|
60
|
-
|
|
61
|
-
Insert new record (no `id`) or update existing (with `id`).
|
|
62
|
-
|
|
63
|
-
**Client:**
|
|
64
|
-
```javascript
|
|
65
|
-
const record = await ws.api.save.{entity}({...fields});
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Server:**
|
|
69
|
-
```javascript
|
|
70
|
-
const record = await db.api.save.{entity}({...fields}, userId);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
**Parameters:**
|
|
74
|
-
| Field | Type | Required | Description |
|
|
75
|
-
|-------|------|----------|-------------|
|
|
76
|
-
| `id` | any | no | Omit for insert, include for update |
|
|
77
|
-
| ...fields | any | varies | Entity-specific fields |
|
|
78
|
-
|
|
79
|
-
**Returns:** Created/updated record
|
|
80
|
-
|
|
81
|
-
**Behavior:**
|
|
82
|
-
- **No `id`**: INSERT new record
|
|
83
|
-
- **With `id`**: UPDATE existing record (partial update supported)
|
|
84
|
-
- Triggers graph rules if configured
|
|
85
|
-
- Generates real-time event
|
|
86
|
-
|
|
87
|
-
**Example:**
|
|
88
|
-
```javascript
|
|
89
|
-
// Insert
|
|
90
|
-
const venue = await ws.api.save.venues({
|
|
91
|
-
name: 'Madison Square Garden',
|
|
92
|
-
address: 'NYC',
|
|
93
|
-
org_id: 1
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Update (partial)
|
|
97
|
-
const updated = await ws.api.save.venues({
|
|
98
|
-
id: 1,
|
|
99
|
-
name: 'Updated Name' // Only updates name
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
### DELETE - Remove Record
|
|
106
|
-
|
|
107
|
-
Delete a record by primary key.
|
|
108
|
-
|
|
109
|
-
**Client:**
|
|
110
|
-
```javascript
|
|
111
|
-
const result = await ws.api.delete.{entity}({id: 1});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
**Server:**
|
|
115
|
-
```javascript
|
|
116
|
-
const result = await db.api.delete.{entity}({id: 1}, userId);
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
**Parameters:**
|
|
120
|
-
| Field | Type | Required | Description |
|
|
121
|
-
|-------|------|----------|-------------|
|
|
122
|
-
| `id` | any | yes | Primary key value |
|
|
123
|
-
|
|
124
|
-
**Returns:** Deleted record
|
|
125
|
-
|
|
126
|
-
**Behavior:**
|
|
127
|
-
- Hard delete (unless soft delete configured)
|
|
128
|
-
- Triggers graph rules if configured
|
|
129
|
-
- Generates real-time event
|
|
130
|
-
|
|
131
|
-
**Example:**
|
|
132
|
-
```javascript
|
|
133
|
-
const deleted = await ws.api.delete.venues({id: 1});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
### LOOKUP - Autocomplete/Typeahead
|
|
139
|
-
|
|
140
|
-
Get label-value pairs for autocomplete inputs.
|
|
141
|
-
|
|
142
|
-
**Client:**
|
|
143
|
-
```javascript
|
|
144
|
-
const options = await ws.api.lookup.{entity}({p_filter: 'search'});
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
**Server:**
|
|
148
|
-
```javascript
|
|
149
|
-
const options = await db.api.lookup.{entity}({p_filter: 'search'}, userId);
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**Parameters:**
|
|
153
|
-
| Field | Type | Required | Description |
|
|
154
|
-
|-------|------|----------|-------------|
|
|
155
|
-
| `p_filter` | string | no | Search term (matches label field) |
|
|
156
|
-
|
|
157
|
-
**Returns:** Array of `{label, value}` objects
|
|
158
|
-
|
|
159
|
-
**Example:**
|
|
160
|
-
```javascript
|
|
161
|
-
const options = await ws.api.lookup.venues({p_filter: 'madison'});
|
|
162
|
-
// [{label: "Madison Square Garden", value: 1}, ...]
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
### SEARCH - Advanced Search with Pagination
|
|
168
|
-
|
|
169
|
-
Search with filters, sorting, and pagination.
|
|
170
|
-
|
|
171
|
-
**Client:**
|
|
172
|
-
```javascript
|
|
173
|
-
const results = await ws.api.search.{entity}({
|
|
174
|
-
filters: {...},
|
|
175
|
-
sort: {field, order},
|
|
176
|
-
page: 1,
|
|
177
|
-
limit: 25
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**Server:**
|
|
182
|
-
```javascript
|
|
183
|
-
const results = await db.api.search.{entity}({...}, userId);
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
**Parameters:**
|
|
187
|
-
| Field | Type | Required | Description |
|
|
188
|
-
|-------|------|----------|-------------|
|
|
189
|
-
| `filters` | object | no | See [Search Operators](#search-operators) |
|
|
190
|
-
| `sort` | object | no | `{field: 'name', order: 'asc' | 'desc'}` |
|
|
191
|
-
| `page` | number | no | Page number (1-indexed, default: 1) |
|
|
192
|
-
| `limit` | number | no | Records per page (default: 25) |
|
|
193
|
-
|
|
194
|
-
**Returns:**
|
|
195
|
-
```javascript
|
|
196
|
-
{
|
|
197
|
-
data: [...], // Array of records
|
|
198
|
-
total: 100, // Total matching records
|
|
199
|
-
page: 1, // Current page
|
|
200
|
-
limit: 25 // Records per page
|
|
201
|
-
}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
**Example:**
|
|
205
|
-
```javascript
|
|
206
|
-
const results = await ws.api.search.venues({
|
|
207
|
-
filters: {
|
|
208
|
-
city: 'New York',
|
|
209
|
-
capacity: {gte: 1000, lt: 5000},
|
|
210
|
-
name: {ilike: '%garden%'},
|
|
211
|
-
_search: 'madison' // Text search across searchable fields
|
|
212
|
-
},
|
|
213
|
-
sort: {field: 'name', order: 'asc'},
|
|
214
|
-
page: 1,
|
|
215
|
-
limit: 25
|
|
216
|
-
});
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## Entity Registration
|
|
222
|
-
|
|
223
|
-
Register an entity to enable all 5 operations via `dzql.register_entity()`.
|
|
224
|
-
|
|
225
|
-
### Full Signature
|
|
226
|
-
|
|
227
|
-
```sql
|
|
228
|
-
SELECT dzql.register_entity(
|
|
229
|
-
p_table_name TEXT,
|
|
230
|
-
p_label_field TEXT,
|
|
231
|
-
p_searchable_fields TEXT[],
|
|
232
|
-
p_fk_includes JSONB DEFAULT '{}'::jsonb,
|
|
233
|
-
p_soft_delete BOOLEAN DEFAULT false,
|
|
234
|
-
p_temporal_fields JSONB DEFAULT '{}'::jsonb,
|
|
235
|
-
p_notification_paths JSONB DEFAULT '{}'::jsonb,
|
|
236
|
-
p_permission_paths JSONB DEFAULT '{}'::jsonb,
|
|
237
|
-
p_graph_rules JSONB DEFAULT '{}'::jsonb
|
|
238
|
-
);
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Parameters
|
|
242
|
-
|
|
243
|
-
| Parameter | Type | Required | Description |
|
|
244
|
-
|-----------|------|----------|-------------|
|
|
245
|
-
| `p_table_name` | TEXT | **yes** | Table name in database |
|
|
246
|
-
| `p_label_field` | TEXT | **yes** | Field used for LOOKUP display |
|
|
247
|
-
| `p_searchable_fields` | TEXT[] | **yes** | Fields searchable by SEARCH (min: 1) |
|
|
248
|
-
| `p_fk_includes` | JSONB | no | Foreign keys to dereference in GET |
|
|
249
|
-
| `p_soft_delete` | BOOLEAN | no | Enable soft delete (default: false) |
|
|
250
|
-
| `p_temporal_fields` | JSONB | no | Temporal field config (valid_from/valid_to) |
|
|
251
|
-
| `p_notification_paths` | JSONB | no | Who receives real-time updates |
|
|
252
|
-
| `p_permission_paths` | JSONB | no | CRUD permission rules |
|
|
253
|
-
| `p_graph_rules` | JSONB | no | Automatic relationship management |
|
|
254
|
-
|
|
255
|
-
### FK Includes
|
|
256
|
-
|
|
257
|
-
Configure which foreign keys to dereference in GET operations:
|
|
258
|
-
|
|
259
|
-
```sql
|
|
260
|
-
-- Single object dereference
|
|
261
|
-
'{"org": "organisations"}' -- venue.org_id -> full org object
|
|
262
|
-
|
|
263
|
-
-- Child array inclusion
|
|
264
|
-
'{"sites": "sites"}' -- Include all child sites (auto-detects FK)
|
|
265
|
-
|
|
266
|
-
-- Multiple
|
|
267
|
-
'{"org": "organisations", "sites": "sites", "venue": "venues"}'
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
**Result example:**
|
|
271
|
-
```javascript
|
|
272
|
-
{
|
|
273
|
-
id: 1,
|
|
274
|
-
name: "Madison Square Garden",
|
|
275
|
-
org_id: 3,
|
|
276
|
-
org: {id: 3, name: "Venue Management", ...}, // Dereferenced
|
|
277
|
-
sites: [ // Child array
|
|
278
|
-
{id: 1, name: "Main Entrance", ...},
|
|
279
|
-
{id: 2, name: "Concourse", ...}
|
|
280
|
-
]
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Temporal Fields
|
|
285
|
-
|
|
286
|
-
Enable temporal relationships with `valid_from`/`valid_to`:
|
|
287
|
-
|
|
288
|
-
```sql
|
|
289
|
-
'{
|
|
290
|
-
"valid_from": "valid_from", -- Column name for start date
|
|
291
|
-
"valid_to": "valid_to" -- Column name for end date
|
|
292
|
-
}'
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
**Usage:**
|
|
296
|
-
```javascript
|
|
297
|
-
// Current relationships (default)
|
|
298
|
-
const rights = await ws.api.get.contractor_rights({id: 1});
|
|
299
|
-
|
|
300
|
-
// Historical relationships
|
|
301
|
-
const past = await ws.api.get.contractor_rights({id: 1, on_date: '2023-01-01'});
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Example Registration
|
|
305
|
-
|
|
306
|
-
```sql
|
|
307
|
-
SELECT dzql.register_entity(
|
|
308
|
-
'venues', -- table name
|
|
309
|
-
'name', -- label field
|
|
310
|
-
array['name', 'address', 'description'], -- searchable
|
|
311
|
-
'{"org": "organisations", "sites": "sites"}', -- FK includes
|
|
312
|
-
false, -- soft delete
|
|
313
|
-
'{}', -- temporal (none)
|
|
314
|
-
'{ -- notifications
|
|
315
|
-
"ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"]
|
|
316
|
-
}',
|
|
317
|
-
'{ -- permissions
|
|
318
|
-
"create": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
319
|
-
"update": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
320
|
-
"delete": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
321
|
-
"view": []
|
|
322
|
-
}',
|
|
323
|
-
'{ -- graph rules
|
|
324
|
-
"on_create": {
|
|
325
|
-
"establish_site": {
|
|
326
|
-
"description": "Create default site",
|
|
327
|
-
"actions": [{
|
|
328
|
-
"type": "create",
|
|
329
|
-
"entity": "sites",
|
|
330
|
-
"data": {"name": "Main Site", "venue_id": "@id"}
|
|
331
|
-
}]
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}'
|
|
335
|
-
);
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
## Search Operators
|
|
341
|
-
|
|
342
|
-
The SEARCH operation supports advanced filtering via the `filters` object.
|
|
343
|
-
|
|
344
|
-
### Operator Reference
|
|
345
|
-
|
|
346
|
-
| Operator | Syntax | Description | Example |
|
|
347
|
-
|----------|--------|-------------|---------|
|
|
348
|
-
| **Exact match** | `field: value` | Equality | `{name: 'Alice'}` |
|
|
349
|
-
| **Greater than** | `{gt: n}` | `>` | `{age: {gt: 18}}` |
|
|
350
|
-
| **Greater or equal** | `{gte: n}` | `>=` | `{age: {gte: 18}}` |
|
|
351
|
-
| **Less than** | `{lt: n}` | `<` | `{age: {lt: 65}}` |
|
|
352
|
-
| **Less or equal** | `{lte: n}` | `<=` | `{age: {lte: 65}}` |
|
|
353
|
-
| **Not equal** | `{neq: v}` | `!=` | `{status: {neq: 'deleted'}}` |
|
|
354
|
-
| **Between** | `{between: [a, b]}` | `BETWEEN a AND b` | `{age: {between: [18, 65]}}` |
|
|
355
|
-
| **LIKE** | `{like: 'pattern'}` | Case-sensitive pattern | `{name: {like: '%Garden%'}}` |
|
|
356
|
-
| **ILIKE** | `{ilike: 'pattern'}` | Case-insensitive pattern | `{name: {ilike: '%garden%'}}` |
|
|
357
|
-
| **IS NULL** | `field: null` | NULL check | `{description: null}` |
|
|
358
|
-
| **IS NOT NULL** | `{not_null: true}` | NOT NULL check | `{description: {not_null: true}}` |
|
|
359
|
-
| **IN array** | `field: [...]` | `IN (...)` | `{city: ['NYC', 'LA']}` |
|
|
360
|
-
| **NOT IN array** | `{not_in: [...]}` | `NOT IN (...)` | `{status: {not_in: ['deleted']}}` |
|
|
361
|
-
| **Text search** | `_search: 'terms'` | Across searchable fields | `{_search: 'madison garden'}` |
|
|
362
|
-
|
|
363
|
-
### Complete Example
|
|
364
|
-
|
|
365
|
-
```javascript
|
|
366
|
-
const results = await ws.api.search.venues({
|
|
367
|
-
filters: {
|
|
368
|
-
// Exact match
|
|
369
|
-
city: 'New York',
|
|
370
|
-
|
|
371
|
-
// Comparison
|
|
372
|
-
capacity: {gte: 1000, lt: 5000},
|
|
373
|
-
|
|
374
|
-
// Pattern matching
|
|
375
|
-
name: {ilike: '%garden%'},
|
|
376
|
-
|
|
377
|
-
// NULL checks
|
|
378
|
-
description: {not_null: true},
|
|
379
|
-
|
|
380
|
-
// Arrays
|
|
381
|
-
categories: ['sports', 'music'],
|
|
382
|
-
status: {not_in: ['deleted', 'closed']},
|
|
383
|
-
|
|
384
|
-
// Text search (across all searchable_fields)
|
|
385
|
-
_search: 'madison square'
|
|
386
|
-
},
|
|
387
|
-
sort: {field: 'capacity', order: 'desc'},
|
|
388
|
-
page: 1,
|
|
389
|
-
limit: 25
|
|
390
|
-
});
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
## Graph Rules
|
|
396
|
-
|
|
397
|
-
Automatically manage entity relationships when data changes.
|
|
398
|
-
|
|
399
|
-
### Structure
|
|
400
|
-
|
|
401
|
-
```jsonb
|
|
402
|
-
{
|
|
403
|
-
"on_create": {
|
|
404
|
-
"rule_name": {
|
|
405
|
-
"description": "Human-readable description",
|
|
406
|
-
"condition": "@after.field = 'value'", // Optional: only run if condition is true
|
|
407
|
-
"actions": [
|
|
408
|
-
{
|
|
409
|
-
"type": "create|update|delete|validate|execute",
|
|
410
|
-
"entity": "target_table", // for create/update/delete
|
|
411
|
-
"data": {"field": "@variable"}, // for create/update
|
|
412
|
-
"match": {"field": "@variable"}, // for update/delete
|
|
413
|
-
"function": "function_name", // for validate/execute
|
|
414
|
-
"params": {"param": "@variable"}, // for validate/execute
|
|
415
|
-
"error_message": "Validation failed" // for validate (optional)
|
|
416
|
-
}
|
|
417
|
-
]
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
"on_update": { /* same structure */ },
|
|
421
|
-
"on_delete": { /* same structure */ }
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### Action Types
|
|
426
|
-
|
|
427
|
-
| Type | Fields | Description |
|
|
428
|
-
|------|--------|-------------|
|
|
429
|
-
| `create` | `entity`, `data` | INSERT new record |
|
|
430
|
-
| `update` | `entity`, `match`, `data` | UPDATE matching records |
|
|
431
|
-
| `delete` | `entity`, `match` | DELETE matching records |
|
|
432
|
-
| `validate` | `function`, `params`, `error_message` | Call validation function, rollback if returns false |
|
|
433
|
-
| `execute` | `function`, `params` | Fire-and-forget function execution |
|
|
434
|
-
|
|
435
|
-
### Variables
|
|
436
|
-
|
|
437
|
-
Variables reference data from the triggering operation:
|
|
438
|
-
|
|
439
|
-
| Variable | Description | Example |
|
|
440
|
-
|----------|-------------|---------|
|
|
441
|
-
| `@user_id` | Current authenticated user | `"created_by": "@user_id"` |
|
|
442
|
-
| `@id` | Primary key of the record | `"org_id": "@id"` |
|
|
443
|
-
| `@field_name` | Any field from the record | `"org_id": "@org_id"` |
|
|
444
|
-
| `@now` | Current timestamp | `"created_at": "@now"` |
|
|
445
|
-
| `@today` | Current date | `"valid_from": "@today"` |
|
|
446
|
-
|
|
447
|
-
### Common Patterns
|
|
448
|
-
|
|
449
|
-
#### Creator Becomes Owner
|
|
450
|
-
```jsonb
|
|
451
|
-
{
|
|
452
|
-
"on_create": {
|
|
453
|
-
"establish_ownership": {
|
|
454
|
-
"description": "Creator becomes member of organisation",
|
|
455
|
-
"actions": [{
|
|
456
|
-
"type": "create",
|
|
457
|
-
"entity": "acts_for",
|
|
458
|
-
"data": {
|
|
459
|
-
"user_id": "@user_id",
|
|
460
|
-
"org_id": "@id",
|
|
461
|
-
"valid_from": "@today"
|
|
462
|
-
}
|
|
463
|
-
}]
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
#### Cascade Delete
|
|
470
|
-
```jsonb
|
|
471
|
-
{
|
|
472
|
-
"on_delete": {
|
|
473
|
-
"cascade_venues": {
|
|
474
|
-
"description": "Delete all venues when org is deleted",
|
|
475
|
-
"actions": [{
|
|
476
|
-
"type": "delete",
|
|
477
|
-
"entity": "venues",
|
|
478
|
-
"match": {"org_id": "@id"}
|
|
479
|
-
}]
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
#### Temporal Transition
|
|
486
|
-
```jsonb
|
|
487
|
-
{
|
|
488
|
-
"on_create": {
|
|
489
|
-
"expire_previous": {
|
|
490
|
-
"description": "End previous temporal relationship",
|
|
491
|
-
"actions": [{
|
|
492
|
-
"type": "update",
|
|
493
|
-
"entity": "acts_for",
|
|
494
|
-
"match": {
|
|
495
|
-
"user_id": "@user_id",
|
|
496
|
-
"org_id": "@org_id",
|
|
497
|
-
"valid_to": null
|
|
498
|
-
},
|
|
499
|
-
"data": {
|
|
500
|
-
"valid_to": "@valid_from"
|
|
501
|
-
}
|
|
502
|
-
}]
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
#### Data Validation
|
|
509
|
-
```jsonb
|
|
510
|
-
{
|
|
511
|
-
"on_create": {
|
|
512
|
-
"validate_positive_price": {
|
|
513
|
-
"description": "Ensure price is positive",
|
|
514
|
-
"actions": [{
|
|
515
|
-
"type": "validate",
|
|
516
|
-
"function": "validate_positive_value",
|
|
517
|
-
"params": {"p_value": "@price"},
|
|
518
|
-
"error_message": "Price must be positive"
|
|
519
|
-
}]
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
**Note:** Validation function must return BOOLEAN:
|
|
526
|
-
```sql
|
|
527
|
-
CREATE FUNCTION validate_positive_value(p_value INT)
|
|
528
|
-
RETURNS BOOLEAN AS $$
|
|
529
|
-
SELECT p_value > 0;
|
|
530
|
-
$$ LANGUAGE sql;
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
#### Conditional Execution
|
|
534
|
-
```jsonb
|
|
535
|
-
{
|
|
536
|
-
"on_update": {
|
|
537
|
-
"prevent_posted_changes": {
|
|
538
|
-
"description": "Prevent modification of posted records",
|
|
539
|
-
"condition": "@before.status = 'posted'",
|
|
540
|
-
"actions": [{
|
|
541
|
-
"type": "validate",
|
|
542
|
-
"function": "always_false",
|
|
543
|
-
"params": {},
|
|
544
|
-
"error_message": "Cannot modify posted records"
|
|
545
|
-
}]
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
**Available in conditions:** `@before.field`, `@after.field`, `@user_id`, and SQL expressions.
|
|
552
|
-
|
|
553
|
-
#### Fire-and-Forget Actions
|
|
554
|
-
```jsonb
|
|
555
|
-
{
|
|
556
|
-
"on_create": {
|
|
557
|
-
"send_notification": {
|
|
558
|
-
"description": "Notify external system",
|
|
559
|
-
"actions": [{
|
|
560
|
-
"type": "execute",
|
|
561
|
-
"function": "log_event",
|
|
562
|
-
"params": {"p_event": "New record created", "p_record_id": "@id"}
|
|
563
|
-
}]
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
**Note:** Execute actions don't affect transaction. Function errors are logged but don't rollback.
|
|
570
|
-
|
|
571
|
-
### Execution
|
|
572
|
-
|
|
573
|
-
- **Atomic**: All rules execute in the same transaction
|
|
574
|
-
- **Sequential**: Actions execute in order within each rule
|
|
575
|
-
- **Rollback**: If any action fails, entire transaction rolls back
|
|
576
|
-
- **Events**: Each action generates its own audit event
|
|
577
|
-
|
|
578
|
-
---
|
|
579
|
-
|
|
580
|
-
## Permission & Notification Paths
|
|
581
|
-
|
|
582
|
-
Paths use a unified syntax for both permissions and notifications.
|
|
583
|
-
|
|
584
|
-
### Path Syntax
|
|
585
|
-
|
|
586
|
-
```
|
|
587
|
-
@field->table[filter]{temporal}.target_field
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**Components:**
|
|
591
|
-
- `@field` - Start from a field in the current record
|
|
592
|
-
- `->table` - Navigate to related table
|
|
593
|
-
- `[filter]` - WHERE clause (`$` = current field value)
|
|
594
|
-
- `{temporal}` - Apply temporal filtering (`{active}` = valid now)
|
|
595
|
-
- `.target_field` - Extract this field as result
|
|
596
|
-
|
|
597
|
-
### Permission Paths
|
|
598
|
-
|
|
599
|
-
Control who can perform CRUD operations:
|
|
600
|
-
|
|
601
|
-
```sql
|
|
602
|
-
'{
|
|
603
|
-
"create": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
604
|
-
"update": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
605
|
-
"delete": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
606
|
-
"view": [] -- Empty array = public access
|
|
607
|
-
}'
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
**Permission types:**
|
|
611
|
-
- `create` - Who can create records
|
|
612
|
-
- `update` - Who can modify records
|
|
613
|
-
- `delete` - Who can remove records
|
|
614
|
-
- `view` - Who can read records (empty = public)
|
|
615
|
-
|
|
616
|
-
**Behavior:**
|
|
617
|
-
- User's `user_id` must be in resolved set of user_ids
|
|
618
|
-
- Checked before operation executes
|
|
619
|
-
- Empty array = allow all
|
|
620
|
-
- Missing permission type = deny all
|
|
621
|
-
|
|
622
|
-
### Notification Paths
|
|
623
|
-
|
|
624
|
-
Determine who receives real-time updates:
|
|
625
|
-
|
|
626
|
-
```sql
|
|
627
|
-
'{
|
|
628
|
-
"ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"],
|
|
629
|
-
"sponsorship": ["@sponsor_org_id->acts_for[org_id=$]{active}.user_id"]
|
|
630
|
-
}'
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
**Behavior:**
|
|
634
|
-
- Resolves to array of user_ids or `null`
|
|
635
|
-
- `null` = broadcast to all authenticated users
|
|
636
|
-
- Array = send only to specified users
|
|
637
|
-
- Multiple paths = union of all resolved user_ids
|
|
638
|
-
|
|
639
|
-
### Path Examples
|
|
640
|
-
|
|
641
|
-
```sql
|
|
642
|
-
-- Direct user reference
|
|
643
|
-
'@user_id'
|
|
644
|
-
|
|
645
|
-
-- Via organization
|
|
646
|
-
'@org_id->acts_for[org_id=$]{active}.user_id'
|
|
647
|
-
|
|
648
|
-
-- Via nested relationship
|
|
649
|
-
'@venue_id->venues.org_id->acts_for[org_id=$]{active}.user_id'
|
|
650
|
-
|
|
651
|
-
-- Via multiple relationships
|
|
652
|
-
'@package_id->packages.owner_org_id->acts_for[org_id=$]{active}.user_id'
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
---
|
|
656
|
-
|
|
657
|
-
## Custom Functions
|
|
658
|
-
|
|
659
|
-
Extend DZQL with custom PostgreSQL or Bun functions.
|
|
660
|
-
|
|
661
|
-
### PostgreSQL Functions
|
|
662
|
-
|
|
663
|
-
Create stored procedures and call via proxy API:
|
|
664
|
-
|
|
665
|
-
```sql
|
|
666
|
-
CREATE OR REPLACE FUNCTION my_function(
|
|
667
|
-
p_user_id INT, -- REQUIRED: First parameter
|
|
668
|
-
p_param TEXT DEFAULT 'default'
|
|
669
|
-
) RETURNS JSONB
|
|
670
|
-
LANGUAGE plpgsql
|
|
671
|
-
SECURITY DEFINER
|
|
672
|
-
AS $$
|
|
673
|
-
BEGIN
|
|
674
|
-
-- Your logic here
|
|
675
|
-
RETURN jsonb_build_object('result', p_param);
|
|
676
|
-
END;
|
|
677
|
-
$$;
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
**Call:**
|
|
681
|
-
```javascript
|
|
682
|
-
const result = await ws.api.my_function({param: 'value'});
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
**Conventions:**
|
|
686
|
-
- First parameter **must** be `p_user_id INT`
|
|
687
|
-
- Can access full PostgreSQL ecosystem
|
|
688
|
-
- Automatically transactional
|
|
689
|
-
- Optional registration in `dzql.registry`
|
|
690
|
-
|
|
691
|
-
### Bun Functions
|
|
692
|
-
|
|
693
|
-
Create JavaScript functions in server:
|
|
694
|
-
|
|
695
|
-
```javascript
|
|
696
|
-
// server/api.js
|
|
697
|
-
export async function myBunFunction(userId, params = {}) {
|
|
698
|
-
const { param = 'default' } = params;
|
|
699
|
-
|
|
700
|
-
// Can use db.api for database access
|
|
701
|
-
// const data = await db.api.get.users({id: userId}, userId);
|
|
702
|
-
|
|
703
|
-
return { result: param };
|
|
704
|
-
}
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
**Server setup:**
|
|
708
|
-
```javascript
|
|
709
|
-
import * as customApi from './server/api.js';
|
|
710
|
-
const server = createServer({ customApi });
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
**Call:**
|
|
714
|
-
```javascript
|
|
715
|
-
const result = await ws.api.myBunFunction({param: 'value'});
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
**Conventions:**
|
|
719
|
-
- First parameter is `userId` (number)
|
|
720
|
-
- Second parameter is `params` object
|
|
721
|
-
- Can access `db.api.*` operations
|
|
722
|
-
- Can use any npm packages
|
|
723
|
-
- Return JSON-serializable data
|
|
724
|
-
|
|
725
|
-
### Function Comparison
|
|
726
|
-
|
|
727
|
-
| Feature | PostgreSQL | Bun |
|
|
728
|
-
|---------|-----------|-----|
|
|
729
|
-
| **Language** | SQL/PL/pgSQL | JavaScript |
|
|
730
|
-
| **Access to** | Database only | Database + npm ecosystem |
|
|
731
|
-
| **Transaction** | Automatic | Manual (via db.api) |
|
|
732
|
-
| **Performance** | Faster (no network) | Slower (WebSocket overhead) |
|
|
733
|
-
| **Use case** | Data-heavy operations | Complex business logic |
|
|
734
|
-
|
|
735
|
-
---
|
|
736
|
-
|
|
737
|
-
## Authentication
|
|
738
|
-
|
|
739
|
-
JWT-based authentication with automatic user_id injection.
|
|
740
|
-
|
|
741
|
-
### Register User
|
|
742
|
-
|
|
743
|
-
```javascript
|
|
744
|
-
const result = await ws.api.register_user({
|
|
745
|
-
email: 'user@example.com',
|
|
746
|
-
password: 'secure-password'
|
|
747
|
-
});
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
**Returns:**
|
|
751
|
-
```javascript
|
|
752
|
-
{
|
|
753
|
-
user_id: 1,
|
|
754
|
-
email: 'user@example.com',
|
|
755
|
-
token: 'eyJ...',
|
|
756
|
-
profile: {...}
|
|
757
|
-
}
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
### Login
|
|
761
|
-
|
|
762
|
-
```javascript
|
|
763
|
-
const result = await ws.api.login_user({
|
|
764
|
-
email: 'user@example.com',
|
|
765
|
-
password: 'password'
|
|
766
|
-
});
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
**Returns:** Same as register
|
|
770
|
-
|
|
771
|
-
### Logout
|
|
772
|
-
|
|
773
|
-
```javascript
|
|
774
|
-
await ws.api.logout();
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
### Token Storage
|
|
778
|
-
|
|
779
|
-
```javascript
|
|
780
|
-
// Save token
|
|
781
|
-
localStorage.setItem('dzql_token', result.token);
|
|
782
|
-
|
|
783
|
-
// Auto-connect with token
|
|
784
|
-
const ws = new WebSocketManager();
|
|
785
|
-
await ws.connect(); // Automatically uses token from localStorage
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
### User ID Injection
|
|
789
|
-
|
|
790
|
-
- **Client**: `user_id` automatically injected from JWT
|
|
791
|
-
- **Server**: `user_id` must be passed explicitly as second parameter
|
|
792
|
-
|
|
793
|
-
```javascript
|
|
794
|
-
// Client
|
|
795
|
-
const user = await ws.api.get.users({id: 1}); // userId auto-injected
|
|
796
|
-
|
|
797
|
-
// Server
|
|
798
|
-
const user = await db.api.get.users({id: 1}, userId); // userId explicit
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
---
|
|
802
|
-
|
|
803
|
-
## Real-time Events
|
|
804
|
-
|
|
805
|
-
All database changes trigger WebSocket events.
|
|
806
|
-
|
|
807
|
-
### Event Flow
|
|
808
|
-
|
|
809
|
-
1. Database trigger fires on INSERT/UPDATE/DELETE
|
|
810
|
-
2. Notification paths resolve affected user_ids
|
|
811
|
-
3. Event written to `dzql.events` table
|
|
812
|
-
4. PostgreSQL NOTIFY on 'dzql' channel
|
|
813
|
-
5. Bun server filters by `notify_users`
|
|
814
|
-
6. WebSocket message sent to affected clients
|
|
815
|
-
|
|
816
|
-
### Listening for Events
|
|
817
|
-
|
|
818
|
-
```javascript
|
|
819
|
-
const unsubscribe = ws.onBroadcast((method, params) => {
|
|
820
|
-
console.log(`Event: ${method}`, params);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
// Stop listening
|
|
824
|
-
unsubscribe();
|
|
825
|
-
```
|
|
826
|
-
|
|
827
|
-
### Event Format
|
|
828
|
-
|
|
829
|
-
**Method:** `"{table}:{operation}"`
|
|
830
|
-
- Examples: `"users:insert"`, `"venues:update"`, `"sites:delete"`
|
|
831
|
-
|
|
832
|
-
**Params:**
|
|
833
|
-
```javascript
|
|
834
|
-
{
|
|
835
|
-
table: 'venues',
|
|
836
|
-
op: 'insert' | 'update' | 'delete',
|
|
837
|
-
pk: {id: 1}, // Primary key
|
|
838
|
-
before: {...}, // Old values (null for insert)
|
|
839
|
-
after: {...}, // New values (null for delete)
|
|
840
|
-
user_id: 123, // Who made the change
|
|
841
|
-
at: '2025-01-01T...', // Timestamp
|
|
842
|
-
notify_users: [1, 2] // Who to notify (null = all)
|
|
843
|
-
}
|
|
844
|
-
```
|
|
845
|
-
|
|
846
|
-
### Event Handling Pattern
|
|
847
|
-
|
|
848
|
-
```javascript
|
|
849
|
-
ws.onBroadcast((method, params) => {
|
|
850
|
-
const data = params.after || params.before;
|
|
851
|
-
|
|
852
|
-
if (method === 'todos:insert') {
|
|
853
|
-
state.todos.push(data);
|
|
854
|
-
} else if (method === 'todos:update') {
|
|
855
|
-
const idx = state.todos.findIndex(t => t.id === data.id);
|
|
856
|
-
if (idx !== -1) state.todos[idx] = data;
|
|
857
|
-
} else if (method === 'todos:delete') {
|
|
858
|
-
state.todos = state.todos.filter(t => t.id !== data.id);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
render();
|
|
862
|
-
});
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
---
|
|
866
|
-
|
|
867
|
-
## Temporal Relationships
|
|
868
|
-
|
|
869
|
-
Handle time-based relationships with `valid_from`/`valid_to` fields.
|
|
870
|
-
|
|
871
|
-
### Configuration
|
|
872
|
-
|
|
873
|
-
```sql
|
|
874
|
-
SELECT dzql.register_entity(
|
|
875
|
-
'contractor_rights',
|
|
876
|
-
'contractor_name',
|
|
877
|
-
array['contractor_name'],
|
|
878
|
-
'{"contractor_org": "organisations", "venue": "venues"}',
|
|
879
|
-
false,
|
|
880
|
-
'{
|
|
881
|
-
"valid_from": "valid_from",
|
|
882
|
-
"valid_to": "valid_to"
|
|
883
|
-
}',
|
|
884
|
-
'{}', '{}'
|
|
885
|
-
);
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
### Usage
|
|
889
|
-
|
|
890
|
-
```javascript
|
|
891
|
-
// Get current relationships (default)
|
|
892
|
-
const rights = await ws.api.get.contractor_rights({id: 1});
|
|
893
|
-
|
|
894
|
-
// Get historical relationships
|
|
895
|
-
const past = await ws.api.get.contractor_rights({
|
|
896
|
-
id: 1,
|
|
897
|
-
on_date: '2023-01-01'
|
|
898
|
-
});
|
|
899
|
-
```
|
|
900
|
-
|
|
901
|
-
### Path Syntax with Temporal
|
|
902
|
-
|
|
903
|
-
```sql
|
|
904
|
-
-- Current relationships only
|
|
905
|
-
'@org_id->acts_for[org_id=$]{active}.user_id'
|
|
906
|
-
|
|
907
|
-
-- All relationships (past and present)
|
|
908
|
-
'@org_id->acts_for[org_id=$].user_id'
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
---
|
|
912
|
-
|
|
913
|
-
## Error Messages
|
|
914
|
-
|
|
915
|
-
Common error messages and their meanings:
|
|
916
|
-
|
|
917
|
-
| Error | Cause | Solution |
|
|
918
|
-
|-------|-------|----------|
|
|
919
|
-
| `"record not found"` | GET on non-existent ID | Check ID exists, handle 404 |
|
|
920
|
-
| `"Permission denied: view on users"` | User not in permission path | Check permissions, authenticate |
|
|
921
|
-
| `"entity users not configured"` | Entity not registered | Call `dzql.register_entity()` |
|
|
922
|
-
| `"Column foo does not exist in table users"` | Invalid filter field | Check searchable_fields config |
|
|
923
|
-
| `"Invalid function name: foo"` | Function doesn't exist | Create function or check spelling |
|
|
924
|
-
| `"Function not found"` | Custom function not registered | Export from api.js or create SQL function |
|
|
925
|
-
| `"Authentication required"` | Not logged in | Call `login_user()` first |
|
|
926
|
-
| `"Invalid token"` | Expired/invalid JWT | Re-authenticate |
|
|
927
|
-
|
|
928
|
-
---
|
|
929
|
-
|
|
930
|
-
## Server-Side API
|
|
931
|
-
|
|
932
|
-
For backend/Bun scripts, use `db.api`:
|
|
933
|
-
|
|
934
|
-
```javascript
|
|
935
|
-
import { db, sql } from 'dzql';
|
|
936
|
-
|
|
937
|
-
// Direct SQL queries
|
|
938
|
-
const users = await sql`SELECT * FROM users WHERE active = true`;
|
|
939
|
-
|
|
940
|
-
// DZQL operations (require explicit userId)
|
|
941
|
-
const user = await db.api.get.users({id: 1}, userId);
|
|
942
|
-
const saved = await db.api.save.users({name: 'John'}, userId);
|
|
943
|
-
const results = await db.api.search.users({filters: {}}, userId);
|
|
944
|
-
const deleted = await db.api.delete.users({id: 1}, userId);
|
|
945
|
-
const options = await db.api.lookup.users({p_filter: 'jo'}, userId);
|
|
946
|
-
|
|
947
|
-
// Custom functions
|
|
948
|
-
const result = await db.api.myCustomFunction({param: 'value'}, userId);
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
**Key difference:** Server-side requires explicit `userId` as second parameter; client-side auto-injects from JWT.
|
|
952
|
-
|
|
953
|
-
---
|
|
954
|
-
|
|
955
|
-
## See Also
|
|
956
|
-
|
|
957
|
-
- [GETTING_STARTED.md](GETTING_STARTED.md) - Hands-on tutorial
|
|
958
|
-
- [CLAUDE.md](../../docs/CLAUDE.md) - AI development guide
|
|
959
|
-
- [README.md](../../README.md) - Project overview
|
|
960
|
-
- [Venues Example](../venues/) - Complete working application
|