dzql 0.5.33 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) 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 +309 -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 +653 -0
  20. package/docs/project-setup.md +456 -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 +166 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/runtime/auth.ts +39 -0
  42. package/src/runtime/db.ts +33 -0
  43. package/src/runtime/errors.ts +51 -0
  44. package/src/runtime/index.ts +98 -0
  45. package/src/runtime/js_functions.ts +63 -0
  46. package/src/runtime/manifest_loader.ts +29 -0
  47. package/src/runtime/namespace.ts +483 -0
  48. package/src/runtime/server.ts +87 -0
  49. package/src/runtime/ws.ts +197 -0
  50. package/src/shared/ir.ts +197 -0
  51. package/tests/client.test.ts +38 -0
  52. package/tests/codegen.test.ts +71 -0
  53. package/tests/compiler.test.ts +45 -0
  54. package/tests/graph_rules.test.ts +173 -0
  55. package/tests/integration/db.test.ts +174 -0
  56. package/tests/integration/e2e.test.ts +65 -0
  57. package/tests/integration/features.test.ts +922 -0
  58. package/tests/integration/full_stack.test.ts +262 -0
  59. package/tests/integration/setup.ts +45 -0
  60. package/tests/ir.test.ts +32 -0
  61. package/tests/namespace.test.ts +395 -0
  62. package/tests/permissions.test.ts +55 -0
  63. package/tests/pinia.test.ts +48 -0
  64. package/tests/realtime.test.ts +22 -0
  65. package/tests/runtime.test.ts +80 -0
  66. package/tests/subscribable_gen.test.ts +72 -0
  67. package/tests/subscribable_reactivity.test.ts +258 -0
  68. package/tests/venues_gen.test.ts +25 -0
  69. package/tsconfig.json +20 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/README.md +0 -90
  72. package/bin/cli.js +0 -727
  73. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  74. package/docs/compiler/CODING_STANDARDS.md +0 -415
  75. package/docs/compiler/COMPARISON.md +0 -673
  76. package/docs/compiler/QUICKSTART.md +0 -326
  77. package/docs/compiler/README.md +0 -134
  78. package/docs/examples/README.md +0 -38
  79. package/docs/examples/blog.sql +0 -160
  80. package/docs/examples/venue-detail-simple.sql +0 -8
  81. package/docs/examples/venue-detail-subscribable.sql +0 -45
  82. package/docs/for-ai/claude-guide.md +0 -1210
  83. package/docs/getting-started/quickstart.md +0 -125
  84. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  85. package/docs/getting-started/tutorial.md +0 -1104
  86. package/docs/guides/atomic-updates.md +0 -299
  87. package/docs/guides/client-stores.md +0 -730
  88. package/docs/guides/composite-primary-keys.md +0 -158
  89. package/docs/guides/custom-functions.md +0 -362
  90. package/docs/guides/drop-semantics.md +0 -554
  91. package/docs/guides/field-defaults.md +0 -240
  92. package/docs/guides/interpreter-vs-compiler.md +0 -237
  93. package/docs/guides/many-to-many.md +0 -929
  94. package/docs/guides/subscriptions.md +0 -537
  95. package/docs/reference/api.md +0 -1373
  96. package/docs/reference/client.md +0 -224
  97. package/src/client/stores/index.js +0 -8
  98. package/src/client/stores/useAppStore.js +0 -285
  99. package/src/client/stores/useWsStore.js +0 -289
  100. package/src/client/ws.js +0 -762
  101. package/src/compiler/cli/compile-example.js +0 -33
  102. package/src/compiler/cli/compile-subscribable.js +0 -43
  103. package/src/compiler/cli/debug-compile.js +0 -44
  104. package/src/compiler/cli/debug-parse.js +0 -26
  105. package/src/compiler/cli/debug-path-parser.js +0 -18
  106. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  107. package/src/compiler/cli/index.js +0 -174
  108. package/src/compiler/codegen/auth-codegen.js +0 -153
  109. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  110. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  111. package/src/compiler/codegen/notification-codegen.js +0 -232
  112. package/src/compiler/codegen/operation-codegen.js +0 -1382
  113. package/src/compiler/codegen/permission-codegen.js +0 -318
  114. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  115. package/src/compiler/compiler.js +0 -371
  116. package/src/compiler/index.js +0 -11
  117. package/src/compiler/parser/entity-parser.js +0 -440
  118. package/src/compiler/parser/path-parser.js +0 -290
  119. package/src/compiler/parser/subscribable-parser.js +0 -244
  120. package/src/database/dzql-core.sql +0 -161
  121. package/src/database/migrations/001_schema.sql +0 -60
  122. package/src/database/migrations/002_functions.sql +0 -890
  123. package/src/database/migrations/003_operations.sql +0 -1135
  124. package/src/database/migrations/004_search.sql +0 -581
  125. package/src/database/migrations/005_entities.sql +0 -730
  126. package/src/database/migrations/006_auth.sql +0 -94
  127. package/src/database/migrations/007_events.sql +0 -133
  128. package/src/database/migrations/008_hello.sql +0 -18
  129. package/src/database/migrations/008a_meta.sql +0 -172
  130. package/src/database/migrations/009_subscriptions.sql +0 -240
  131. package/src/database/migrations/010_atomic_updates.sql +0 -157
  132. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  133. package/src/index.js +0 -40
  134. package/src/server/api.js +0 -9
  135. package/src/server/db.js +0 -442
  136. package/src/server/index.js +0 -317
  137. package/src/server/logger.js +0 -259
  138. package/src/server/mcp.js +0 -594
  139. package/src/server/meta-route.js +0 -251
  140. package/src/server/namespace.js +0 -292
  141. package/src/server/subscriptions.js +0 -351
  142. 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