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.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,1210 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Quick Reference Card
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
DZQL QUICK REFERENCE
|
|
9
|
-
====================
|
|
10
|
-
|
|
11
|
-
5 Operations: get, save, delete, lookup, search
|
|
12
|
-
2 Modes: Interpreter (runtime) | Compiler (static SQL)
|
|
13
|
-
Client API: ws.api.{operation}.{entity}(params)
|
|
14
|
-
Server API: db.api.{operation}.{entity}(params, userId)
|
|
15
|
-
|
|
16
|
-
Entity Registration:
|
|
17
|
-
dzql.register_entity(
|
|
18
|
-
table_name, -- 'todos'
|
|
19
|
-
label_field, -- 'title' (for lookups)
|
|
20
|
-
searchable_fields, -- ARRAY['title', 'description']
|
|
21
|
-
fk_includes, -- '{"org": "organisations"}'
|
|
22
|
-
soft_delete, -- false
|
|
23
|
-
temporal_fields, -- '{}'
|
|
24
|
-
notification_paths, -- '{"ownership": ["@org_id->acts_for..."]}'
|
|
25
|
-
permission_paths, -- '{"view": [], "create": [...]}'
|
|
26
|
-
graph_rules, -- '{"on_create": {...}, "many_to_many": {...}, "primary_key": [...]}'
|
|
27
|
-
field_defaults -- '{"owner_id": "@user_id"}'
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
Composite Primary Keys:
|
|
31
|
-
graph_rules: '{"primary_key": ["entity_type", "entity_id"]}'
|
|
32
|
-
- GET/DELETE accept JSONB: get_table(user_id, '{"col1": "val", "col2": 123}')
|
|
33
|
-
- SAVE detects insert/update by checking if all PK fields exist
|
|
34
|
-
- Columns ending with _id are cast to ::int, others stay text
|
|
35
|
-
|
|
36
|
-
M2M id_field naming: tag_ids (singular + _ids), NOT tags_ids
|
|
37
|
-
Permission [] = public, omitted = denied
|
|
38
|
-
Path syntax: @field->table[filter]{temporal}.target_field
|
|
39
|
-
|
|
40
|
-
Compile: dzql compile entities.sql -o compiled/
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Project Overview
|
|
44
|
-
|
|
45
|
-
DZQL is a PostgreSQL-powered framework that eliminates CRUD boilerplate by providing automatic database operations, real-time WebSocket synchronization, and graph-based relationship management. The core concept: register an entity in PostgreSQL and instantly get 5 standard operations (get, save, delete, lookup, search) plus real-time notifications with zero code.
|
|
46
|
-
|
|
47
|
-
## Architecture
|
|
48
|
-
|
|
49
|
-
### Three-Layer Stack
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
Client (Browser) Server (Bun) Database (PostgreSQL)
|
|
53
|
-
WebSocketManager <-> WebSocket Handler <-> Generic Operations
|
|
54
|
-
Proxy API JSON-RPC Router Stored Procedures
|
|
55
|
-
Real-time Events NOTIFY/LISTEN Graph Rules Engine
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Key Architectural Patterns
|
|
59
|
-
|
|
60
|
-
1. **Nested Proxy API**: Both client (`ws.api.save.venues()`) and server (`db.api.save.venues()`) use identical proxy-based APIs that dynamically route to the correct operation
|
|
61
|
-
2. **Generic Operations**: All CRUD operations flow through `dzql.generic_exec()` which handles permissions, graph rules, and event generation
|
|
62
|
-
3. **Single Channel NOTIFY**: All real-time events flow through one PostgreSQL NOTIFY channel ('dzql') with intelligent user targeting
|
|
63
|
-
4. **Graph Rules**: Entity relationships are managed declaratively through JSON configuration that executes automatically on data changes
|
|
64
|
-
|
|
65
|
-
### Database-Centric Design
|
|
66
|
-
|
|
67
|
-
The framework treats PostgreSQL as the source of truth for:
|
|
68
|
-
- **Entity Configuration**: `dzql.entities` table stores metadata (searchable fields, permissions, temporal config)
|
|
69
|
-
- **Event Log**: `dzql.events` table provides complete audit trail with targeted notification data
|
|
70
|
-
- **Permission Paths**: JSON path expressions resolve which users can perform operations
|
|
71
|
-
- **Notification Paths**: JSON path expressions determine who receives real-time updates
|
|
72
|
-
- **Graph Rules**: Declarative relationship management executed within transactions
|
|
73
|
-
|
|
74
|
-
## Development Commands
|
|
75
|
-
|
|
76
|
-
### Venues Example (Primary Development)
|
|
77
|
-
```bash
|
|
78
|
-
bun venues:db # Start PostgreSQL in Docker (clean slate every time)
|
|
79
|
-
bun venues # Start Bun server with hot reload
|
|
80
|
-
bun venues:test # Run full test suite
|
|
81
|
-
bun venues:logs # View PostgreSQL logs
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Logging Configuration
|
|
85
|
-
|
|
86
|
-
DZQL uses a category-based logging system with configurable log levels. Configure via environment variables:
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
# Set overall log level (ERROR, WARN, INFO, DEBUG, TRACE)
|
|
90
|
-
LOG_LEVEL=DEBUG bun venues
|
|
91
|
-
|
|
92
|
-
# Set per-category levels
|
|
93
|
-
LOG_CATEGORIES="ws:debug,db:trace,auth:info,server:info,notify:debug" bun venues
|
|
94
|
-
|
|
95
|
-
# Or set all categories to same level
|
|
96
|
-
LOG_CATEGORIES="*:trace" bun venues
|
|
97
|
-
|
|
98
|
-
# Disable colors
|
|
99
|
-
NO_COLOR=1 bun venues
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**Available categories:**
|
|
103
|
-
- `ws` - WebSocket connections and RPC calls (green)
|
|
104
|
-
- `db` - Database operations and queries (magenta)
|
|
105
|
-
- `auth` - Authentication events (yellow)
|
|
106
|
-
- `server` - Server startup and shutdown (blue)
|
|
107
|
-
- `notify` - Real-time NOTIFY events (magenta)
|
|
108
|
-
|
|
109
|
-
**Log levels (lowest to highest):**
|
|
110
|
-
- `ERROR` - Errors only
|
|
111
|
-
- `WARN` - Warnings and errors
|
|
112
|
-
- `INFO` - Informational messages (default in development)
|
|
113
|
-
- `DEBUG` - Debug information including request/response
|
|
114
|
-
- `TRACE` - Very detailed tracing
|
|
115
|
-
|
|
116
|
-
**Default behavior:**
|
|
117
|
-
- Development: `INFO` level for all categories
|
|
118
|
-
- Production: `WARN` level for all categories
|
|
119
|
-
- Test: `ERROR` level (suppresses most output)
|
|
120
|
-
|
|
121
|
-
### Testing Individual Test Files
|
|
122
|
-
```bash
|
|
123
|
-
cd packages/venues
|
|
124
|
-
bun test tests/domain.test.js # Test basic CRUD operations
|
|
125
|
-
bun test tests/permissions.test.js # Test permission system
|
|
126
|
-
bun test tests/graph_rules.test.js # Test relationship management
|
|
127
|
-
bun test tests/notifications.test.js # Test real-time events
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Alternative Rights Example
|
|
131
|
-
```bash
|
|
132
|
-
bun db # Start PostgreSQL for rights example
|
|
133
|
-
bun server # Start rights server
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Full Stack Development
|
|
137
|
-
```bash
|
|
138
|
-
bun dev # Run both client and server concurrently
|
|
139
|
-
bun client # Client-only development server
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Project Structure
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
packages/
|
|
146
|
-
├── dzql/ # Core framework
|
|
147
|
-
│ └── src/
|
|
148
|
-
│ ├── database/migrations/ # PostgreSQL migrations (numbered)
|
|
149
|
-
│ │ ├── 001_schema.sql # Core tables (entities, events)
|
|
150
|
-
│ │ ├── 002_functions.sql # Path resolution helpers
|
|
151
|
-
│ │ ├── 003_operations.sql # Generic CRUD + graph rules
|
|
152
|
-
│ │ ├── 004_search.sql # Advanced search functionality
|
|
153
|
-
│ │ ├── 005_entities.sql # Entity registration
|
|
154
|
-
│ │ └── 006_auth.sql # JWT authentication
|
|
155
|
-
│ ├── server/
|
|
156
|
-
│ │ ├── db.js # PostgreSQL connection + proxy API
|
|
157
|
-
│ │ ├── ws.js # WebSocket handlers + JSON-RPC
|
|
158
|
-
│ │ └── index.js # Server factory
|
|
159
|
-
│ └── client/
|
|
160
|
-
│ └── ws.js # WebSocket client + proxy API
|
|
161
|
-
├── venues/ # Example application
|
|
162
|
-
│ ├── server/
|
|
163
|
-
│ │ ├── index.js # Application entry point
|
|
164
|
-
│ │ └── api.js # Custom Bun functions
|
|
165
|
-
│ ├── database/
|
|
166
|
-
│ │ ├── docker-compose.yml # PostgreSQL setup
|
|
167
|
-
│ │ └── init_db/
|
|
168
|
-
│ │ └── 009_venues_domain.sql # Domain entities
|
|
169
|
-
│ └── tests/ # Comprehensive test suite
|
|
170
|
-
└── client/ # Shared client utilities
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Core Concepts
|
|
174
|
-
|
|
175
|
-
### 1. Nested Proxy API Pattern
|
|
176
|
-
|
|
177
|
-
The same API works on both client and server:
|
|
178
|
-
|
|
179
|
-
```javascript
|
|
180
|
-
// Client (WebSocket)
|
|
181
|
-
const venue = await ws.api.get.venues({id: 1});
|
|
182
|
-
const saved = await ws.api.save.venues({name: 'New Venue'});
|
|
183
|
-
|
|
184
|
-
// Server (Direct PostgreSQL)
|
|
185
|
-
const venue = await db.api.get.venues({id: 1}, userId);
|
|
186
|
-
const saved = await db.api.save.venues({name: 'New Venue'}, userId);
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Implementation details:
|
|
190
|
-
- Operations: `get`, `save`, `delete`, `lookup`, `search`
|
|
191
|
-
- Custom functions are accessed directly: `ws.api.customFunction({params})`
|
|
192
|
-
- All operations require authentication (except `login_user` and `register_user`)
|
|
193
|
-
- Server-side requires explicit `userId` parameter; client-side injects automatically
|
|
194
|
-
|
|
195
|
-
### 2. Entity Registration
|
|
196
|
-
|
|
197
|
-
Entities are configured via `dzql.register_entity()` which sets up everything needed:
|
|
198
|
-
|
|
199
|
-
```sql
|
|
200
|
-
SELECT dzql.register_entity(
|
|
201
|
-
'venues', -- table name
|
|
202
|
-
'name', -- label field for lookups
|
|
203
|
-
array['name', 'address'], -- searchable fields
|
|
204
|
-
'{"org": "organisations", "sites": "sites"}', -- foreign keys to dereference + child arrays
|
|
205
|
-
false, -- soft delete enabled
|
|
206
|
-
'{}', -- temporal fields config
|
|
207
|
-
'{"ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"]}', -- notification paths
|
|
208
|
-
'{"view": [], "create": [...]}', -- permission paths
|
|
209
|
-
'{...}' -- graph rules
|
|
210
|
-
);
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
**FK Includes Syntax:**
|
|
214
|
-
- Single object dereference: `"org": "organisations"` - Follows FK to get full org object
|
|
215
|
-
- Child array inclusion: `"sites": "sites"` - Includes all child records (auto-detects FK relationship)
|
|
216
|
-
- Example result from `get` operation:
|
|
217
|
-
```json
|
|
218
|
-
{
|
|
219
|
-
"id": 1,
|
|
220
|
-
"name": "Madison Square Garden",
|
|
221
|
-
"org": { "id": 3, "name": "Venue Management", ... },
|
|
222
|
-
"sites": [
|
|
223
|
-
{ "id": 1, "name": "Main Entrance", ... },
|
|
224
|
-
{ "id": 2, "name": "Concourse Level", ... }
|
|
225
|
-
]
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### 3. Path Resolution Syntax
|
|
230
|
-
|
|
231
|
-
Paths are used for both notifications and permissions:
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
@field->table[filter]{temporal}.target_field
|
|
235
|
-
|
|
236
|
-
Examples:
|
|
237
|
-
@org_id->acts_for[org_id=$]{active}.user_id
|
|
238
|
-
@venue_id->venues.org_id->acts_for[org_id=$]{active}.user_id
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Components:
|
|
242
|
-
- `@field` - Start from record field
|
|
243
|
-
- `->table` - Navigate to related table
|
|
244
|
-
- `[filter]` - WHERE clause (`$` = current value)
|
|
245
|
-
- `{temporal}` - Apply temporal filtering (`{active}` = valid now)
|
|
246
|
-
- `.field` - Extract this field as result
|
|
247
|
-
|
|
248
|
-
### 4. Graph Rules
|
|
249
|
-
|
|
250
|
-
Automatic relationship management executed in transactions:
|
|
251
|
-
|
|
252
|
-
```jsonb
|
|
253
|
-
{
|
|
254
|
-
"on_create": {
|
|
255
|
-
"rule_name": {
|
|
256
|
-
"description": "Human-readable description",
|
|
257
|
-
"actions": [{
|
|
258
|
-
"type": "create|update|delete",
|
|
259
|
-
"entity": "target_table",
|
|
260
|
-
"data": {"field": "@variable"}, // for create/update
|
|
261
|
-
"match": {"field": "@variable"} // for update/delete
|
|
262
|
-
}]
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Variables available: `@user_id`, `@id`, `@field_name`, `@now`, `@today`
|
|
269
|
-
|
|
270
|
-
**Available Action Types:**
|
|
271
|
-
|
|
272
|
-
| Action | Purpose | Required Fields | Rollback on Error |
|
|
273
|
-
|--------|---------|----------------|-------------------|
|
|
274
|
-
| `create` | Create related record | `entity`, `data` | ✅ Yes |
|
|
275
|
-
| `update` | Update related record | `entity`, `match`, `data` | ✅ Yes |
|
|
276
|
-
| `delete` | Delete related record | `entity`, `match` | ✅ Yes |
|
|
277
|
-
| `validate` | Block operation if validation fails | `function`, `params`, `error_message` | ✅ Yes |
|
|
278
|
-
| `execute` | Fire-and-forget function call | `function`, `params` | ❌ No |
|
|
279
|
-
|
|
280
|
-
#### Graph Rules: Advanced Features
|
|
281
|
-
|
|
282
|
-
**Conditional Execution**
|
|
283
|
-
|
|
284
|
-
Rules can include conditions that determine if they execute:
|
|
285
|
-
|
|
286
|
-
```jsonb
|
|
287
|
-
{
|
|
288
|
-
"on_update": {
|
|
289
|
-
"prevent_modification": {
|
|
290
|
-
"condition": "@before.status = 'posted'",
|
|
291
|
-
"actions": [{
|
|
292
|
-
"type": "validate",
|
|
293
|
-
"function": "always_false",
|
|
294
|
-
"params": {},
|
|
295
|
-
"error_message": "Cannot modify a posted record"
|
|
296
|
-
}]
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
**Condition Variables:**
|
|
303
|
-
- `@before.field` - Value before update (null for create)
|
|
304
|
-
- `@after.field` - Value after update/create (null for delete)
|
|
305
|
-
- `@user_id` - Current user ID
|
|
306
|
-
- `@id` - Record ID
|
|
307
|
-
- Standard SQL expressions: `=`, `!=`, `AND`, `OR`, `>`, `<`, `>=`, `<=`
|
|
308
|
-
|
|
309
|
-
**Validate Action**
|
|
310
|
-
|
|
311
|
-
Call validation functions that can block operations:
|
|
312
|
-
|
|
313
|
-
```jsonb
|
|
314
|
-
{
|
|
315
|
-
"on_create": {
|
|
316
|
-
"validate_positive": {
|
|
317
|
-
"description": "Ensure value is positive",
|
|
318
|
-
"actions": [{
|
|
319
|
-
"type": "validate",
|
|
320
|
-
"function": "validate_positive_value",
|
|
321
|
-
"params": {"p_value": "@value"},
|
|
322
|
-
"error_message": "Value must be positive"
|
|
323
|
-
}]
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
**Validation function signature:**
|
|
330
|
-
```sql
|
|
331
|
-
CREATE FUNCTION validate_positive_value(p_value INT)
|
|
332
|
-
RETURNS BOOLEAN
|
|
333
|
-
LANGUAGE sql AS $$
|
|
334
|
-
SELECT p_value > 0;
|
|
335
|
-
$$;
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
Validation functions must:
|
|
339
|
-
- Return BOOLEAN (true = pass, false = fail)
|
|
340
|
-
- Use named parameters matching the `params` object
|
|
341
|
-
- Be deterministic for consistent results
|
|
342
|
-
|
|
343
|
-
**Execute Action**
|
|
344
|
-
|
|
345
|
-
Call custom functions as side effects (fire-and-forget):
|
|
346
|
-
|
|
347
|
-
```jsonb
|
|
348
|
-
{
|
|
349
|
-
"on_create": {
|
|
350
|
-
"send_notification": {
|
|
351
|
-
"description": "Notify external system",
|
|
352
|
-
"actions": [{
|
|
353
|
-
"type": "execute",
|
|
354
|
-
"function": "send_email_notification",
|
|
355
|
-
"params": {"p_email": "@email", "p_name": "@name"}
|
|
356
|
-
}]
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Execute functions can return JSONB or void. Errors are logged as warnings but don't block the operation or rollback the transaction.
|
|
363
|
-
|
|
364
|
-
#### Complex Validation Example: Double-Entry Bookkeeping
|
|
365
|
-
|
|
366
|
-
**Requirement**: Journal entries must be balanced (debits = credits) before posting
|
|
367
|
-
|
|
368
|
-
**Validation function that queries related tables:**
|
|
369
|
-
```sql
|
|
370
|
-
CREATE FUNCTION validate_journal_entry_balanced(p_entry_id INT)
|
|
371
|
-
RETURNS BOOLEAN AS $$
|
|
372
|
-
DECLARE
|
|
373
|
-
v_total_debits DECIMAL;
|
|
374
|
-
v_total_credits DECIMAL;
|
|
375
|
-
BEGIN
|
|
376
|
-
-- Query related journal_lines table
|
|
377
|
-
SELECT
|
|
378
|
-
COALESCE(SUM(debit_amount), 0),
|
|
379
|
-
COALESCE(SUM(credit_amount), 0)
|
|
380
|
-
INTO v_total_debits, v_total_credits
|
|
381
|
-
FROM journal_lines
|
|
382
|
-
WHERE entry_id = p_entry_id;
|
|
383
|
-
|
|
384
|
-
-- Check if balanced and non-zero
|
|
385
|
-
RETURN v_total_debits = v_total_credits AND v_total_debits > 0;
|
|
386
|
-
END;
|
|
387
|
-
$$ LANGUAGE plpgsql;
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
**Entity registration with multiple validation rules:**
|
|
391
|
-
```sql
|
|
392
|
-
SELECT dzql.register_entity(
|
|
393
|
-
'journal_entries',
|
|
394
|
-
'description',
|
|
395
|
-
ARRAY['description'],
|
|
396
|
-
'{"lines": "journal_lines"}', -- Include child lines
|
|
397
|
-
false, '{}', '{}', '{}',
|
|
398
|
-
jsonb_build_object(
|
|
399
|
-
'on_update', jsonb_build_object(
|
|
400
|
-
-- Rule 1: Validate balanced entry
|
|
401
|
-
'validate_balanced_on_post', jsonb_build_object(
|
|
402
|
-
'description', 'Ensure entry is balanced before posting',
|
|
403
|
-
'condition', '@after.status = ''posted'' AND @before.status = ''draft''',
|
|
404
|
-
'actions', jsonb_build_array(
|
|
405
|
-
jsonb_build_object(
|
|
406
|
-
'type', 'validate',
|
|
407
|
-
'function', 'validate_journal_entry_balanced',
|
|
408
|
-
'params', jsonb_build_object('p_entry_id', '@id'),
|
|
409
|
-
'error_message', 'Cannot post unbalanced entry - debits must equal credits'
|
|
410
|
-
)
|
|
411
|
-
)
|
|
412
|
-
),
|
|
413
|
-
-- Rule 2: Check fiscal period is open
|
|
414
|
-
'check_fiscal_period_open', jsonb_build_object(
|
|
415
|
-
'description', 'Prevent posting to closed periods',
|
|
416
|
-
'condition', '@after.status = ''posted''',
|
|
417
|
-
'actions', jsonb_build_array(
|
|
418
|
-
jsonb_build_object(
|
|
419
|
-
'type', 'validate',
|
|
420
|
-
'function', 'is_fiscal_period_open',
|
|
421
|
-
'params', jsonb_build_object('p_period_id', '@fiscal_period_id'),
|
|
422
|
-
'error_message', 'Cannot post to a closed fiscal period'
|
|
423
|
-
)
|
|
424
|
-
)
|
|
425
|
-
),
|
|
426
|
-
-- Rule 3: Prevent modifying posted entries
|
|
427
|
-
'prevent_modify_posted', jsonb_build_object(
|
|
428
|
-
'description', 'Posted entries are immutable',
|
|
429
|
-
'condition', '@before.status = ''posted''',
|
|
430
|
-
'actions', jsonb_build_array(
|
|
431
|
-
jsonb_build_object(
|
|
432
|
-
'type', 'validate',
|
|
433
|
-
'function', 'always_false',
|
|
434
|
-
'params', jsonb_build_object(),
|
|
435
|
-
'error_message', 'Cannot modify a posted journal entry'
|
|
436
|
-
)
|
|
437
|
-
)
|
|
438
|
-
)
|
|
439
|
-
)
|
|
440
|
-
)
|
|
441
|
-
);
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
**Key features demonstrated:**
|
|
445
|
-
- Multiple validation rules on same trigger
|
|
446
|
-
- Conditions control when validations run
|
|
447
|
-
- Validation functions query related tables
|
|
448
|
-
- Clear error messages for business rules
|
|
449
|
-
|
|
450
|
-
#### Migration from PostgreSQL Triggers to Graph Rules Validation
|
|
451
|
-
|
|
452
|
-
**Before (Trigger approach):**
|
|
453
|
-
```sql
|
|
454
|
-
CREATE TRIGGER journal_entry_validation
|
|
455
|
-
BEFORE UPDATE ON journal_entries
|
|
456
|
-
FOR EACH ROW
|
|
457
|
-
EXECUTE FUNCTION check_journal_entry_balanced();
|
|
458
|
-
|
|
459
|
-
CREATE OR REPLACE FUNCTION check_journal_entry_balanced()
|
|
460
|
-
RETURNS TRIGGER AS $$
|
|
461
|
-
BEGIN
|
|
462
|
-
IF NEW.status = 'posted' AND OLD.status = 'draft' THEN
|
|
463
|
-
IF NOT validate_journal_entry_balanced(NEW.id) THEN
|
|
464
|
-
RAISE EXCEPTION 'Cannot post unbalanced journal entry';
|
|
465
|
-
END IF;
|
|
466
|
-
END IF;
|
|
467
|
-
RETURN NEW;
|
|
468
|
-
END;
|
|
469
|
-
$$ LANGUAGE plpgsql;
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
**After (Graph rules approach):**
|
|
473
|
-
```sql
|
|
474
|
-
SELECT dzql.register_entity(
|
|
475
|
-
'journal_entries', 'description', ARRAY['description'],
|
|
476
|
-
'{}', false, '{}', '{}', '{}',
|
|
477
|
-
jsonb_build_object(
|
|
478
|
-
'on_update', jsonb_build_object(
|
|
479
|
-
'validate_balanced', jsonb_build_object(
|
|
480
|
-
'condition', '@after.status = ''posted'' AND @before.status = ''draft''',
|
|
481
|
-
'actions', jsonb_build_array(
|
|
482
|
-
jsonb_build_object(
|
|
483
|
-
'type', 'validate',
|
|
484
|
-
'function', 'validate_journal_entry_balanced',
|
|
485
|
-
'params', jsonb_build_object('p_entry_id', '@id'),
|
|
486
|
-
'error_message', 'Cannot post unbalanced journal entry'
|
|
487
|
-
)
|
|
488
|
-
)
|
|
489
|
-
)
|
|
490
|
-
)
|
|
491
|
-
)
|
|
492
|
-
);
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
**Advantages:**
|
|
496
|
-
- ✅ Visible in entity registration (no separate trigger objects)
|
|
497
|
-
- ✅ Declarative and easier to understand
|
|
498
|
-
- ✅ Conditional execution built-in
|
|
499
|
-
- ✅ Testable (can call validation function directly)
|
|
500
|
-
- ✅ All entity config in one place
|
|
501
|
-
|
|
502
|
-
**When to still use triggers:**
|
|
503
|
-
- Complex multi-step validation with loops/cursors
|
|
504
|
-
- Need to modify NEW record before saving
|
|
505
|
-
- Side effects that must happen in same transaction (use execute action instead for side effects)
|
|
506
|
-
- Integration with legacy PostgreSQL systems
|
|
507
|
-
|
|
508
|
-
### 5. Real-Time Event Flow
|
|
509
|
-
|
|
510
|
-
1. Database trigger fires on INSERT/UPDATE/DELETE
|
|
511
|
-
2. Notification paths resolve affected user_ids
|
|
512
|
-
3. Event written to `dzql.events` with `notify_users` array
|
|
513
|
-
4. PostgreSQL NOTIFY on 'dzql' channel
|
|
514
|
-
5. Bun server filters by `notify_users` (null = all authenticated users)
|
|
515
|
-
6. WebSocket sends event as JSON-RPC method: `{table}:{op}`
|
|
516
|
-
|
|
517
|
-
## Writing Tests
|
|
518
|
-
|
|
519
|
-
Tests use Bun's built-in test runner with these patterns:
|
|
520
|
-
|
|
521
|
-
### Test Structure
|
|
522
|
-
```javascript
|
|
523
|
-
import { test, expect, beforeAll, afterAll } from "bun:test";
|
|
524
|
-
import { sql, db } from "dzql";
|
|
525
|
-
|
|
526
|
-
const PREFIX = `TEST_${Date.now()}`; // Unique prefix for test isolation
|
|
527
|
-
let testUserId;
|
|
528
|
-
|
|
529
|
-
beforeAll(async () => {
|
|
530
|
-
// Create test user
|
|
531
|
-
const result = await sql`SELECT register_user(...)`;
|
|
532
|
-
testUserId = result[0].user_data.user_id;
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
afterAll(async () => {
|
|
536
|
-
// Clean up test data in dependency order (children first)
|
|
537
|
-
await sql`DELETE FROM child_table WHERE parent_id IN (...)`;
|
|
538
|
-
await sql`DELETE FROM parent_table WHERE name LIKE ${PREFIX + '%'}`;
|
|
539
|
-
await sql`DELETE FROM users WHERE id = ${testUserId}`;
|
|
540
|
-
});
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Key Testing Patterns
|
|
544
|
-
- Use unique PREFIX with timestamp to avoid conflicts
|
|
545
|
-
- Clean up in correct FK dependency order (children before parents)
|
|
546
|
-
- Use `db.api` for testing DZQL operations (not WebSocket)
|
|
547
|
-
- Direct SQL via `sql` tagged template for setup/cleanup
|
|
548
|
-
- Server-side API requires explicit `userId` as second parameter
|
|
549
|
-
|
|
550
|
-
## Adding New Entities
|
|
551
|
-
|
|
552
|
-
1. **Create table in domain SQL file** (`packages/venues/database/init_db/009_venues_domain.sql`)
|
|
553
|
-
2. **Register entity** with `dzql.register_entity()` in same file
|
|
554
|
-
3. **Configure permissions** using path syntax
|
|
555
|
-
4. **Add graph rules** if needed for relationship management
|
|
556
|
-
5. **Write tests** following existing patterns in `packages/venues/tests/`
|
|
557
|
-
|
|
558
|
-
No TypeScript types, API routes, or resolvers needed - everything is handled by generic operations.
|
|
559
|
-
|
|
560
|
-
## Adding Custom Functions
|
|
561
|
-
|
|
562
|
-
### PostgreSQL Functions (Stored Procedures)
|
|
563
|
-
```sql
|
|
564
|
-
CREATE OR REPLACE FUNCTION my_function(p_user_id INT, p_param TEXT DEFAULT 'default')
|
|
565
|
-
RETURNS JSONB
|
|
566
|
-
LANGUAGE plpgsql
|
|
567
|
-
SECURITY DEFINER
|
|
568
|
-
AS $$
|
|
569
|
-
BEGIN
|
|
570
|
-
-- Function logic
|
|
571
|
-
RETURN jsonb_build_object('result', p_param);
|
|
572
|
-
END;
|
|
573
|
-
$$;
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
Call from client: `await ws.api.my_function({param: 'value'})`
|
|
577
|
-
|
|
578
|
-
### Bun Functions (JavaScript)
|
|
579
|
-
```javascript
|
|
580
|
-
// packages/venues/server/api.js
|
|
581
|
-
export async function myBunFunction(userId, params = {}) {
|
|
582
|
-
const { param = 'default' } = params;
|
|
583
|
-
// Can use db.api for database access
|
|
584
|
-
return { result: param };
|
|
585
|
-
}
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
Call from client: `await ws.api.myBunFunction({param: 'value'})`
|
|
589
|
-
|
|
590
|
-
**Both types:**
|
|
591
|
-
- First parameter is always `user_id` (auto-injected on client)
|
|
592
|
-
- Require authentication
|
|
593
|
-
- Use same proxy API syntax
|
|
594
|
-
- Return JSON-serializable data
|
|
595
|
-
|
|
596
|
-
## CLI Access (invokej)
|
|
597
|
-
|
|
598
|
-
DZQL operations are available via CLI using `invokej` for testing and scripting:
|
|
599
|
-
|
|
600
|
-
```bash
|
|
601
|
-
# List all entities
|
|
602
|
-
invokej dzql.entities
|
|
603
|
-
|
|
604
|
-
# Search entities
|
|
605
|
-
invokej dzql.search organisations '{"query": "test"}'
|
|
606
|
-
|
|
607
|
-
# Get entity by ID (use primary key field, usually "id")
|
|
608
|
-
invokej dzql.get venues '{"id": 1}'
|
|
609
|
-
|
|
610
|
-
# Create/update entity
|
|
611
|
-
invokej dzql.save venues '{"name": "New Venue", "org_id": 1, "address": "123 Main St"}'
|
|
612
|
-
|
|
613
|
-
# Delete entity
|
|
614
|
-
invokej dzql.delete venues '{"id": 1}'
|
|
615
|
-
|
|
616
|
-
# Lookup (autocomplete)
|
|
617
|
-
invokej dzql.lookup organisations '{"query": "acme"}'
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
**CLI Notes:**
|
|
621
|
-
- All commands use default `user_id=1` for permissions
|
|
622
|
-
- Arguments must be valid JSON strings
|
|
623
|
-
- Mirrors MCP server functionality exactly
|
|
624
|
-
- Defined in `tasks.js` at project root
|
|
625
|
-
|
|
626
|
-
## Important Conventions
|
|
627
|
-
|
|
628
|
-
### Database
|
|
629
|
-
- Core DZQL tables use `dzql` schema
|
|
630
|
-
- Application tables use `public` schema
|
|
631
|
-
- Migration files are numbered sequentially (001, 002, etc.)
|
|
632
|
-
- Domain-specific SQL files start at 009
|
|
633
|
-
- **No `created_at`/`updated_at` columns** - use `dzql.events` table for complete audit trail
|
|
634
|
-
|
|
635
|
-
### Code Style
|
|
636
|
-
- ES modules (type: "module" in package.json)
|
|
637
|
-
- Async/await for all database operations
|
|
638
|
-
- Tagged templates for SQL queries (`sql` from postgres package)
|
|
639
|
-
- Proxy patterns for API routing
|
|
640
|
-
|
|
641
|
-
### Real-time Events
|
|
642
|
-
- Listen using `ws.onBroadcast((method, params) => {})`
|
|
643
|
-
- Method format: `{table}:{operation}` (e.g., "venues:update")
|
|
644
|
-
- Params include: `{table, op, pk, data, user_id, at}`
|
|
645
|
-
- `data` contains: new state for insert/update, `null` for delete
|
|
646
|
-
- Target users via notification paths or broadcast to all
|
|
647
|
-
|
|
648
|
-
### Permissions
|
|
649
|
-
- Empty view permission array `[]` = public read access
|
|
650
|
-
- Non-empty arrays = restricted to resolved user_ids
|
|
651
|
-
- Permissions checked before operations execute
|
|
652
|
-
- Use path syntax to traverse relationships
|
|
653
|
-
|
|
654
|
-
## Common Gotchas
|
|
655
|
-
|
|
656
|
-
1. **Server vs Client API**: Server `db.api` requires explicit `userId` as second parameter; client `ws.api` auto-injects from JWT
|
|
657
|
-
2. **Test Cleanup Order**: Always delete FK children before parents to avoid constraint violations
|
|
658
|
-
3. **Temporal Filtering**: Use `{active}` in paths for current relationships; omit for all time
|
|
659
|
-
4. **Graph Rule Variables**: Use `@` prefix for all variables (`@user_id`, `@id`, `@field_name`)
|
|
660
|
-
5. **Permission Paths**: Empty array means "allow all", missing permission type means "deny all"
|
|
661
|
-
6. **NOTIFY Filtering**: `notify_users: null` broadcasts to all authenticated users; array targets specific user_ids
|
|
662
|
-
|
|
663
|
-
---
|
|
664
|
-
|
|
665
|
-
## Database Schema Reference
|
|
666
|
-
|
|
667
|
-
### Core DZQL Tables
|
|
668
|
-
|
|
669
|
-
#### `dzql.entities`
|
|
670
|
-
Stores entity configuration metadata:
|
|
671
|
-
```sql
|
|
672
|
-
TABLE dzql.entities {
|
|
673
|
-
table_name TEXT PRIMARY KEY,
|
|
674
|
-
label_field TEXT NOT NULL,
|
|
675
|
-
searchable_fields TEXT[] NOT NULL,
|
|
676
|
-
fk_includes JSONB DEFAULT '{}'::jsonb,
|
|
677
|
-
soft_delete BOOLEAN DEFAULT false,
|
|
678
|
-
temporal_fields JSONB DEFAULT '{}'::jsonb,
|
|
679
|
-
notification_paths JSONB DEFAULT '{}'::jsonb,
|
|
680
|
-
permission_paths JSONB DEFAULT '{}'::jsonb,
|
|
681
|
-
graph_rules JSONB DEFAULT '{}'::jsonb
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
#### `dzql.events`
|
|
686
|
-
Complete audit trail with real-time notification data:
|
|
687
|
-
```sql
|
|
688
|
-
TABLE dzql.events {
|
|
689
|
-
event_id BIGSERIAL PRIMARY KEY,
|
|
690
|
-
context_id TEXT, -- For catchup queries
|
|
691
|
-
table_name TEXT NOT NULL,
|
|
692
|
-
op TEXT NOT NULL, -- 'insert' | 'update' | 'delete'
|
|
693
|
-
pk JSONB NOT NULL, -- Primary key: {id: 1}
|
|
694
|
-
data JSONB, -- Record data (new state for insert/update, null for delete)
|
|
695
|
-
user_id INT, -- Who made the change
|
|
696
|
-
notify_users INT[], -- Who to notify (null = all)
|
|
697
|
-
at TIMESTAMPTZ DEFAULT NOW()
|
|
698
|
-
}
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
#### `dzql.registry`
|
|
702
|
-
Allowed custom functions (optional):
|
|
703
|
-
```sql
|
|
704
|
-
TABLE dzql.registry {
|
|
705
|
-
function_name TEXT PRIMARY KEY,
|
|
706
|
-
description TEXT
|
|
707
|
-
}
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
---
|
|
711
|
-
|
|
712
|
-
## Common Error Messages
|
|
713
|
-
|
|
714
|
-
### Error Dictionary
|
|
715
|
-
|
|
716
|
-
| Error Message | Cause | Solution |
|
|
717
|
-
|---------------|-------|----------|
|
|
718
|
-
| `"record not found"` | GET operation on non-existent ID | Check ID exists, handle 404 case |
|
|
719
|
-
| `"Permission denied: view on users"` | User not in permission path result | Verify user has access, check permission paths |
|
|
720
|
-
| `"Permission denied: create on venues"` | User can't create records | Add user to create permission path |
|
|
721
|
-
| `"entity users not configured"` | Table not registered with DZQL | Call `dzql.register_entity()` |
|
|
722
|
-
| `"Column foo does not exist in table users"` | Invalid filter field in SEARCH | Check `searchable_fields` configuration |
|
|
723
|
-
| `"Invalid function name: foo"` | Custom function doesn't exist | Create function or check spelling |
|
|
724
|
-
| `"Function not found"` | Function not exported or not in DB | Export from api.js or CREATE FUNCTION |
|
|
725
|
-
| `"Authentication required"` | Not logged in | Call `login_user()` first |
|
|
726
|
-
| `"Invalid token"` | Expired or malformed JWT | Re-authenticate with `login_user()` |
|
|
727
|
-
| `"Duplicate key violates unique constraint"` | Inserting duplicate unique value | Check for existing record first |
|
|
728
|
-
| `"Foreign key violation"` | Referenced record doesn't exist | Create parent record before child |
|
|
729
|
-
| `"Invalid JSON"` | Malformed JSON in request | Validate JSON syntax |
|
|
730
|
-
|
|
731
|
-
### Error Handling Pattern
|
|
732
|
-
|
|
733
|
-
```javascript
|
|
734
|
-
try {
|
|
735
|
-
const user = await ws.api.get.users({id: userId});
|
|
736
|
-
} catch (error) {
|
|
737
|
-
if (error.message === 'record not found') {
|
|
738
|
-
// Handle missing record
|
|
739
|
-
} else if (error.message.includes('Permission denied')) {
|
|
740
|
-
// Handle unauthorized access
|
|
741
|
-
} else {
|
|
742
|
-
// Handle unexpected errors
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
---
|
|
748
|
-
|
|
749
|
-
## Event Structure Reference
|
|
750
|
-
|
|
751
|
-
### WebSocket Event Format
|
|
752
|
-
|
|
753
|
-
**Method:** `"{table}:{operation}"`
|
|
754
|
-
- Examples: `"venues:insert"`, `"users:update"`, `"sites:delete"`
|
|
755
|
-
|
|
756
|
-
**Params Structure:**
|
|
757
|
-
```javascript
|
|
758
|
-
{
|
|
759
|
-
table: 'venues', // Table name
|
|
760
|
-
op: 'insert', // Operation: 'insert' | 'update' | 'delete'
|
|
761
|
-
pk: {id: 1}, // Primary key object
|
|
762
|
-
data: { // Record data (new state for insert/update, null for delete)
|
|
763
|
-
id: 1,
|
|
764
|
-
name: 'New Name',
|
|
765
|
-
address: 'New Address'
|
|
766
|
-
},
|
|
767
|
-
user_id: 123, // User who made the change
|
|
768
|
-
at: '2025-01-01T12:00:00Z' // Timestamp
|
|
769
|
-
}
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
**Event data by operation:**
|
|
773
|
-
| Operation | `data` field contains |
|
|
774
|
-
|-----------|----------------------|
|
|
775
|
-
| `insert` | Full new record |
|
|
776
|
-
| `update` | Full updated record (new state only) |
|
|
777
|
-
| `delete` | `null` |
|
|
778
|
-
|
|
779
|
-
**Note:** The `notify_users` field is used internally for routing but is stripped from the broadcast message sent to clients.
|
|
780
|
-
|
|
781
|
-
### Using Event Data
|
|
782
|
-
|
|
783
|
-
```javascript
|
|
784
|
-
ws.onBroadcast((method, params) => {
|
|
785
|
-
const { table, op, pk, data } = params;
|
|
786
|
-
|
|
787
|
-
// For insert
|
|
788
|
-
if (method === 'venues:insert') {
|
|
789
|
-
const newRecord = data;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// For update
|
|
793
|
-
if (method === 'venues:update') {
|
|
794
|
-
const updatedRecord = data;
|
|
795
|
-
// Note: only new state is available, not the previous state
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// For delete
|
|
799
|
-
if (method === 'venues:delete') {
|
|
800
|
-
// data is null for delete, use pk to identify the deleted record
|
|
801
|
-
const deletedId = pk.id;
|
|
802
|
-
}
|
|
803
|
-
});
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
---
|
|
807
|
-
|
|
808
|
-
## Decision Trees
|
|
809
|
-
|
|
810
|
-
### When to Use Graph Rules vs Manual Operations
|
|
811
|
-
|
|
812
|
-
**Use Graph Rules When:**
|
|
813
|
-
- ✅ Pattern repeats consistently (always create X when Y is created)
|
|
814
|
-
- ✅ Relationship is declarative (cascade delete, ownership transfer)
|
|
815
|
-
- ✅ Action is atomic with parent operation
|
|
816
|
-
- ✅ Business rule applies system-wide
|
|
817
|
-
- ✅ Need automatic execution within same transaction
|
|
818
|
-
|
|
819
|
-
**Use Manual Operations When:**
|
|
820
|
-
- ❌ Logic is complex with multiple conditions
|
|
821
|
-
- ❌ Requires external API calls
|
|
822
|
-
- ❌ Need user confirmation before action
|
|
823
|
-
- ❌ Action is optional or contextual
|
|
824
|
-
- ❌ Involves asynchronous processing
|
|
825
|
-
|
|
826
|
-
**Examples:**
|
|
827
|
-
|
|
828
|
-
✅ **Good for Graph Rules:**
|
|
829
|
-
```jsonb
|
|
830
|
-
// Creator becomes owner
|
|
831
|
-
"on_create": {
|
|
832
|
-
"establish_ownership": {
|
|
833
|
-
"actions": [{
|
|
834
|
-
"type": "create",
|
|
835
|
-
"entity": "acts_for",
|
|
836
|
-
"data": {"user_id": "@user_id", "org_id": "@id"}
|
|
837
|
-
}]
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
❌ **Bad for Graph Rules:**
|
|
843
|
-
```javascript
|
|
844
|
-
// Send welcome email, create Stripe customer, notify Slack
|
|
845
|
-
// Too many external dependencies - do manually
|
|
846
|
-
export async function createOrganisation(userId, params) {
|
|
847
|
-
const org = await db.api.save.organisations(params, userId);
|
|
848
|
-
await sendWelcomeEmail(org);
|
|
849
|
-
await createStripeCustomer(org);
|
|
850
|
-
await notifySlack(org);
|
|
851
|
-
return org;
|
|
852
|
-
}
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
### PostgreSQL Functions vs Bun Functions
|
|
856
|
-
|
|
857
|
-
**Use PostgreSQL Functions When:**
|
|
858
|
-
- ✅ Data-heavy operations (aggregations, complex queries)
|
|
859
|
-
- ✅ Need transactional guarantees
|
|
860
|
-
- ✅ Performance critical (no network overhead)
|
|
861
|
-
- ✅ Pure database logic
|
|
862
|
-
- ✅ Reusable across multiple applications
|
|
863
|
-
|
|
864
|
-
**Use Bun Functions When:**
|
|
865
|
-
- ✅ Complex business logic
|
|
866
|
-
- ✅ Need external API calls
|
|
867
|
-
- ✅ Require npm packages
|
|
868
|
-
- ✅ Easier to test/debug in JavaScript
|
|
869
|
-
- ✅ Rapid prototyping
|
|
870
|
-
|
|
871
|
-
**Example Decision:**
|
|
872
|
-
|
|
873
|
-
✅ **PostgreSQL - Data aggregation:**
|
|
874
|
-
```sql
|
|
875
|
-
CREATE FUNCTION get_venue_stats(p_user_id INT, p_venue_id INT)
|
|
876
|
-
RETURNS JSONB AS $$
|
|
877
|
-
SELECT jsonb_build_object(
|
|
878
|
-
'total_sites', COUNT(s.id),
|
|
879
|
-
'total_events', COUNT(e.id),
|
|
880
|
-
'revenue', SUM(e.revenue)
|
|
881
|
-
)
|
|
882
|
-
FROM sites s
|
|
883
|
-
LEFT JOIN events e ON e.site_id = s.id
|
|
884
|
-
WHERE s.venue_id = p_venue_id;
|
|
885
|
-
$$ LANGUAGE sql;
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
✅ **Bun - External API integration:**
|
|
889
|
-
```javascript
|
|
890
|
-
export async function sendInvitation(userId, params) {
|
|
891
|
-
const { email, org_id } = params;
|
|
892
|
-
|
|
893
|
-
// Send via SendGrid
|
|
894
|
-
await sendgrid.send({...});
|
|
895
|
-
|
|
896
|
-
// Create invitation record
|
|
897
|
-
await db.api.save.invitations({
|
|
898
|
-
email, org_id, sent_by: userId
|
|
899
|
-
}, userId);
|
|
900
|
-
|
|
901
|
-
return { success: true };
|
|
902
|
-
}
|
|
903
|
-
```
|
|
904
|
-
|
|
905
|
-
### Notification Paths vs Broadcast All
|
|
906
|
-
|
|
907
|
-
**Use Targeted Notification Paths When:**
|
|
908
|
-
- ✅ Only specific users should see the change
|
|
909
|
-
- ✅ Data is sensitive (private org data)
|
|
910
|
-
- ✅ Need to reduce notification noise
|
|
911
|
-
- ✅ Clear ownership/membership model
|
|
912
|
-
|
|
913
|
-
**Use Broadcast All (null) When:**
|
|
914
|
-
- ✅ Public data everyone should see
|
|
915
|
-
- ✅ No ownership model
|
|
916
|
-
- ✅ Simplicity is priority
|
|
917
|
-
- ✅ Small user base
|
|
918
|
-
|
|
919
|
-
**Example:**
|
|
920
|
-
|
|
921
|
-
✅ **Targeted (private venues):**
|
|
922
|
-
```sql
|
|
923
|
-
SELECT dzql.register_entity(
|
|
924
|
-
'venues', 'name', array['name'],
|
|
925
|
-
'{}', false, '{}',
|
|
926
|
-
'{
|
|
927
|
-
"ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"]
|
|
928
|
-
}' -- Only org members notified
|
|
929
|
-
);
|
|
930
|
-
```
|
|
931
|
-
|
|
932
|
-
✅ **Broadcast (public events):**
|
|
933
|
-
```sql
|
|
934
|
-
SELECT dzql.register_entity(
|
|
935
|
-
'public_events', 'name', array['name'],
|
|
936
|
-
'{}', false, '{}',
|
|
937
|
-
'{}' -- Empty = notify all authenticated users
|
|
938
|
-
);
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
---
|
|
942
|
-
|
|
943
|
-
## Transaction Boundaries
|
|
944
|
-
|
|
945
|
-
### What Runs Atomically
|
|
946
|
-
|
|
947
|
-
**Single Transaction Includes:**
|
|
948
|
-
1. Primary operation (save/delete)
|
|
949
|
-
2. All graph rule actions for that operation
|
|
950
|
-
3. Event log writing
|
|
951
|
-
4. Trigger execution
|
|
952
|
-
|
|
953
|
-
**Example Flow:**
|
|
954
|
-
```javascript
|
|
955
|
-
// User creates organisation
|
|
956
|
-
await ws.api.save.organisations({name: 'Acme Corp'});
|
|
957
|
-
|
|
958
|
-
// PostgreSQL executes in ONE transaction:
|
|
959
|
-
// 1. INSERT INTO organisations
|
|
960
|
-
// 2. Graph rule: INSERT INTO acts_for (creator becomes owner)
|
|
961
|
-
// 3. INSERT INTO dzql.events
|
|
962
|
-
// 4. NOTIFY 'dzql'
|
|
963
|
-
// Either all succeed or all rollback
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
### Transaction Rollback
|
|
967
|
-
|
|
968
|
-
If any step fails, **entire transaction rolls back**:
|
|
969
|
-
|
|
970
|
-
```sql
|
|
971
|
-
-- This graph rule will rollback the org creation if site creation fails
|
|
972
|
-
"on_create": {
|
|
973
|
-
"create_default_site": {
|
|
974
|
-
"actions": [{
|
|
975
|
-
"type": "create",
|
|
976
|
-
"entity": "sites",
|
|
977
|
-
"data": {
|
|
978
|
-
"venue_id": "@id",
|
|
979
|
-
"invalid_field": "@foo" -- ERROR: invalid_field doesn't exist
|
|
980
|
-
}
|
|
981
|
-
}]
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
-- Result: Organisation is NOT created, error is thrown
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
### Multiple Operations Are Separate Transactions
|
|
988
|
-
|
|
989
|
-
```javascript
|
|
990
|
-
// These are TWO separate transactions
|
|
991
|
-
const org = await ws.api.save.organisations({name: 'Acme'}); // Transaction 1
|
|
992
|
-
const venue = await ws.api.save.venues({org_id: org.id}); // Transaction 2
|
|
993
|
-
|
|
994
|
-
// If venue creation fails, org still exists
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
To make multiple operations atomic, use a PostgreSQL function:
|
|
998
|
-
|
|
999
|
-
```sql
|
|
1000
|
-
CREATE FUNCTION create_org_with_venue(p_user_id INT, p_org_name TEXT, p_venue_name TEXT)
|
|
1001
|
-
RETURNS JSONB AS $$
|
|
1002
|
-
DECLARE
|
|
1003
|
-
v_org RECORD;
|
|
1004
|
-
v_venue RECORD;
|
|
1005
|
-
BEGIN
|
|
1006
|
-
INSERT INTO organisations (name) VALUES (p_org_name) RETURNING * INTO v_org;
|
|
1007
|
-
INSERT INTO venues (name, org_id) VALUES (p_venue_name, v_org.id) RETURNING * INTO v_venue;
|
|
1008
|
-
|
|
1009
|
-
RETURN jsonb_build_object('org', to_jsonb(v_org), 'venue', to_jsonb(v_venue));
|
|
1010
|
-
END;
|
|
1011
|
-
$$ LANGUAGE plpgsql;
|
|
1012
|
-
```
|
|
1013
|
-
|
|
1014
|
-
---
|
|
1015
|
-
|
|
1016
|
-
## FK Includes Edge Cases
|
|
1017
|
-
|
|
1018
|
-
### Circular References
|
|
1019
|
-
|
|
1020
|
-
**Problem:** A→B and B→A causes infinite recursion
|
|
1021
|
-
|
|
1022
|
-
```sql
|
|
1023
|
-
-- organisations.parent_org_id -> organisations
|
|
1024
|
-
-- organisations.child_orgs <- organisations
|
|
1025
|
-
SELECT dzql.register_entity(
|
|
1026
|
-
'organisations', 'name', array['name'],
|
|
1027
|
-
'{"parent": "organisations", "children": "organisations"}' -- CIRCULAR!
|
|
1028
|
-
);
|
|
1029
|
-
```
|
|
1030
|
-
|
|
1031
|
-
**Solution:** Only include one direction
|
|
1032
|
-
```sql
|
|
1033
|
-
'{"parent": "organisations"}' -- Parent only, not children
|
|
1034
|
-
```
|
|
1035
|
-
|
|
1036
|
-
### Deeply Nested Includes
|
|
1037
|
-
|
|
1038
|
-
**Problem:** Performance degrades with deep nesting
|
|
1039
|
-
|
|
1040
|
-
```sql
|
|
1041
|
-
-- venue -> org -> parent_org -> parent_org -> ...
|
|
1042
|
-
'{"org": "organisations"}' -- This only derefs 1 level
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
**Behavior:**
|
|
1046
|
-
- FK includes dereference **1 level only**
|
|
1047
|
-
- Nested includes (org.parent_org) are NOT automatically included
|
|
1048
|
-
- To include nested, configure in each entity separately
|
|
1049
|
-
|
|
1050
|
-
### Performance Implications
|
|
1051
|
-
|
|
1052
|
-
| FK Includes | Query Complexity | Recommendation |
|
|
1053
|
-
|-------------|------------------|----------------|
|
|
1054
|
-
| None | SELECT * FROM table | Fastest |
|
|
1055
|
-
| 1-2 single objects | 1-2 JOINs | Good |
|
|
1056
|
-
| 3+ single objects | 3+ JOINs | Consider splitting |
|
|
1057
|
-
| 1 child array | 1 subquery | Good |
|
|
1058
|
-
| 2+ child arrays | 2+ subqueries | Slow - avoid |
|
|
1059
|
-
|
|
1060
|
-
**Optimization Strategy:**
|
|
1061
|
-
- Only include FKs you actually need
|
|
1062
|
-
- Avoid including large child arrays unless necessary
|
|
1063
|
-
- Consider separate queries for child arrays
|
|
1064
|
-
- Use pagination for child arrays
|
|
1065
|
-
|
|
1066
|
-
---
|
|
1067
|
-
|
|
1068
|
-
## Security Checklist
|
|
1069
|
-
|
|
1070
|
-
### When Building DZQL Applications
|
|
1071
|
-
|
|
1072
|
-
**Authentication:**
|
|
1073
|
-
- ✅ Always require `login_user()` before operations
|
|
1074
|
-
- ✅ Store JWT in secure storage (not localStorage for production)
|
|
1075
|
-
- ✅ Set `JWT_EXPIRES_IN` to reasonable duration (7d default)
|
|
1076
|
-
- ✅ Regenerate `JWT_SECRET` for production (never use default)
|
|
1077
|
-
- ✅ Validate token on every operation (automatic in DZQL)
|
|
1078
|
-
|
|
1079
|
-
**Permissions:**
|
|
1080
|
-
- ✅ Configure `permission_paths` for all non-public entities
|
|
1081
|
-
- ✅ Use empty array `[]` only for truly public data
|
|
1082
|
-
- ✅ Test permission paths with different user roles
|
|
1083
|
-
- ✅ Never trust client-side filtering - always enforce server-side
|
|
1084
|
-
- ✅ Use `{active}` temporal filtering in permission paths
|
|
1085
|
-
|
|
1086
|
-
**Graph Rules:**
|
|
1087
|
-
- ✅ Validate graph rule variables exist
|
|
1088
|
-
- ✅ Test rollback behavior (ensure atomicity)
|
|
1089
|
-
- ✅ Avoid complex logic in graph rules (use functions instead)
|
|
1090
|
-
- ✅ Document cascade delete behavior
|
|
1091
|
-
|
|
1092
|
-
**Input Validation:**
|
|
1093
|
-
- ✅ DZQL validates entity schema automatically
|
|
1094
|
-
- ✅ Add custom validation in PostgreSQL functions if needed
|
|
1095
|
-
- ✅ Use CHECK constraints for business rules
|
|
1096
|
-
- ✅ Never expose raw error messages to client
|
|
1097
|
-
|
|
1098
|
-
**Rate Limiting:**
|
|
1099
|
-
- ⚠️ DZQL doesn't include rate limiting (v0.1.0)
|
|
1100
|
-
- ✅ Add rate limiting middleware for production
|
|
1101
|
-
- ✅ Limit login attempts
|
|
1102
|
-
- ✅ Limit operations per user/minute
|
|
1103
|
-
|
|
1104
|
-
**Error Handling:**
|
|
1105
|
-
- ✅ Catch all errors in client
|
|
1106
|
-
- ✅ Log errors server-side
|
|
1107
|
-
- ✅ Never expose stack traces in production
|
|
1108
|
-
- ✅ Return generic error messages to client
|
|
1109
|
-
|
|
1110
|
-
---
|
|
1111
|
-
|
|
1112
|
-
## Performance Guidelines
|
|
1113
|
-
|
|
1114
|
-
### Index Recommendations
|
|
1115
|
-
|
|
1116
|
-
**Always Index:**
|
|
1117
|
-
- ✅ Primary keys (automatic)
|
|
1118
|
-
- ✅ Foreign keys used in paths
|
|
1119
|
-
- ✅ Fields in `searchable_fields`
|
|
1120
|
-
- ✅ Temporal fields (`valid_from`, `valid_to`)
|
|
1121
|
-
- ✅ Fields used in permission path filters
|
|
1122
|
-
|
|
1123
|
-
```sql
|
|
1124
|
-
-- Example indexes for venues entity
|
|
1125
|
-
CREATE INDEX idx_venues_org_id ON venues(org_id);
|
|
1126
|
-
CREATE INDEX idx_venues_name ON venues(name); -- searchable field
|
|
1127
|
-
CREATE INDEX idx_sites_venue_id ON sites(venue_id); -- for FK includes
|
|
1128
|
-
```
|
|
1129
|
-
|
|
1130
|
-
### Searchable Fields Impact
|
|
1131
|
-
|
|
1132
|
-
**Performance Cost:**
|
|
1133
|
-
- Each searchable field adds to `_search` query cost
|
|
1134
|
-
- Text search uses `ILIKE` which can be slow without indexes
|
|
1135
|
-
- Limit to 3-5 truly searchable fields
|
|
1136
|
-
|
|
1137
|
-
```sql
|
|
1138
|
-
-- Good: 3 searchable fields
|
|
1139
|
-
array['name', 'address', 'city']
|
|
1140
|
-
|
|
1141
|
-
-- Bad: Too many fields
|
|
1142
|
-
array['name', 'address', 'city', 'description', 'notes', 'tags', 'metadata']
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
### FK Includes Cost
|
|
1146
|
-
|
|
1147
|
-
| Include Type | Cost | Query |
|
|
1148
|
-
|--------------|------|-------|
|
|
1149
|
-
| Single object | 1 JOIN | Fast |
|
|
1150
|
-
| Child array (small) | 1 subquery | Moderate |
|
|
1151
|
-
| Child array (large) | 1 subquery + many rows | Slow |
|
|
1152
|
-
| Multiple arrays | N subqueries | Very slow |
|
|
1153
|
-
|
|
1154
|
-
**Optimization:**
|
|
1155
|
-
```sql
|
|
1156
|
-
-- Good: One FK dereference
|
|
1157
|
-
'{"org": "organisations"}'
|
|
1158
|
-
|
|
1159
|
-
-- Good: Small child array (<100 records)
|
|
1160
|
-
'{"sites": "sites"}'
|
|
1161
|
-
|
|
1162
|
-
-- Bad: Multiple large child arrays
|
|
1163
|
-
'{"sites": "sites", "events": "events", "contractors": "contractors"}'
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
### Graph Rules Complexity
|
|
1167
|
-
|
|
1168
|
-
**Fast Graph Rules:**
|
|
1169
|
-
- ✅ Single action per rule
|
|
1170
|
-
- ✅ Simple match conditions
|
|
1171
|
-
- ✅ Direct variable references
|
|
1172
|
-
|
|
1173
|
-
**Slow Graph Rules:**
|
|
1174
|
-
- ❌ Multiple chained actions
|
|
1175
|
-
- ❌ Complex match conditions
|
|
1176
|
-
- ❌ Nested graph rules triggering other graph rules
|
|
1177
|
-
|
|
1178
|
-
```jsonb
|
|
1179
|
-
// Good: Simple cascade
|
|
1180
|
-
"on_delete": {
|
|
1181
|
-
"cascade": {
|
|
1182
|
-
"actions": [{"type": "delete", "entity": "sites", "match": {"venue_id": "@id"}}]
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// Bad: Complex chain
|
|
1187
|
-
"on_create": {
|
|
1188
|
-
"rule1": {"actions": [...]}, // Triggers more operations
|
|
1189
|
-
"rule2": {"actions": [...]}, // Which trigger more operations
|
|
1190
|
-
"rule3": {"actions": [...]} // Slows down creation significantly
|
|
1191
|
-
}
|
|
1192
|
-
```
|
|
1193
|
-
|
|
1194
|
-
### Query Optimization Tips
|
|
1195
|
-
|
|
1196
|
-
1. **Limit searchable fields** to truly searchable content
|
|
1197
|
-
2. **Index foreign keys** used in joins and paths
|
|
1198
|
-
3. **Avoid large FK includes** in list operations
|
|
1199
|
-
4. **Use pagination** (keep `limit` ≤ 100)
|
|
1200
|
-
5. **Filter before including** FKs when possible
|
|
1201
|
-
6. **Monitor slow queries** via PostgreSQL logs
|
|
1202
|
-
|
|
1203
|
-
---
|
|
1204
|
-
|
|
1205
|
-
## Additional Resources
|
|
1206
|
-
|
|
1207
|
-
- **API Reference**: See [API Reference](../reference/api.md) for complete API documentation
|
|
1208
|
-
- **Tutorial**: See [Getting Started Tutorial](../getting-started/tutorial.md) for hands-on guide
|
|
1209
|
-
- **Examples**: See `packages/venues/` for complete working application
|
|
1210
|
-
- **Tests**: See `packages/venues/tests/` for comprehensive test patterns
|