dzql 0.1.2 → 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/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