dzql 0.5.33 → 0.6.0

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.
Files changed (150) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +293 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +641 -0
  20. package/docs/project-setup.md +432 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. package/src/server/ws.js +0 -573
@@ -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