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,1104 +0,0 @@
1
- # Getting Started with DZQL - Practical Guide
2
-
3
- DZQL is a PostgreSQL framework that gives you **atomic real-time updates** via WebSocket. Every database change broadcasts instantly to all connected clients. Zero boilerplate.
4
-
5
- > **See also:** [API Reference](../reference/api.md) for complete API documentation | [Claude Guide](../for-ai/claude-guide.md) for AI development guide
6
-
7
- ## The Core Pattern
8
-
9
- 1. **Schema = API**: Define a table → DZQL auto-creates CRUD endpoints
10
- 2. **Atomic Updates**: Every change is one transaction, broadcasts to all clients
11
- 3. **Real-time Sync**: Listen to broadcasts, update local state, re-render
12
- 4. **No Polling**: Changes propagate instantly, no stale data
13
-
14
- ```javascript
15
- // Listen to broadcasts
16
- ws.onBroadcast((method, params) => {
17
- if (method === "todos:insert") state.todos.push(params.data)
18
- else if (method === "todos:update") {
19
- const idx = state.todos.findIndex(t => t.id === params.data.id)
20
- if (idx !== -1) state.todos[idx] = params.data
21
- }
22
- else if (method === "todos:delete") {
23
- state.todos = state.todos.filter(t => t.id !== params.data.id)
24
- }
25
- render() // One render function, called on every change
26
- })
27
- ```
28
-
29
- That's the entire pattern. All clients stay in sync automatically.
30
-
31
- ## Prerequisites
32
-
33
- - **Bun** 1.0+ (Node.js not required)
34
- - **Docker** and **Docker Compose**
35
- - A code editor
36
-
37
- ## Quick Start (10 minutes)
38
-
39
- ### 1. Create Project
40
-
41
- ```bash
42
- mkdir my-dzql-app
43
- cd my-dzql-app
44
- bun init
45
- bun add dzql
46
-
47
- # Create directories
48
- mkdir -p public init_db
49
- ```
50
-
51
- ### 2. Project Structure
52
-
53
- ```
54
- my-dzql-app/
55
- ├── index.js # DZQL server
56
- ├── public/
57
- │ ├── index.html # HTML markup
58
- │ ├── index.css # Styles
59
- │ └── app.js # JavaScript
60
- ├── init_db/
61
- │ └── 001_domain.sql # Your schema
62
- ├── compose.yml # PostgreSQL config
63
- ├── package.json
64
- └── app.test.ts # Tests (optional)
65
- ```
66
-
67
- ### 3. Docker Setup
68
-
69
- Create `compose.yml`:
70
-
71
- ```yaml
72
- services:
73
- postgres:
74
- image: postgres:latest
75
- environment:
76
- POSTGRES_USER: dzql
77
- POSTGRES_PASSWORD: dzql
78
- POSTGRES_DB: dzql
79
- volumes:
80
- - ./node_modules/dzql/src/database/migrations/001_schema.sql:/docker-entrypoint-initdb.d/001_schema.sql:ro
81
- - ./node_modules/dzql/src/database/migrations/002_functions.sql:/docker-entrypoint-initdb.d/002_functions.sql:ro
82
- - ./node_modules/dzql/src/database/migrations/003_operations.sql:/docker-entrypoint-initdb.d/003_operations.sql:ro
83
- - ./node_modules/dzql/src/database/migrations/004_search.sql:/docker-entrypoint-initdb.d/004_search.sql:ro
84
- - ./node_modules/dzql/src/database/migrations/005_entities.sql:/docker-entrypoint-initdb.d/005_entities.sql:ro
85
- - ./node_modules/dzql/src/database/migrations/006_auth.sql:/docker-entrypoint-initdb.d/006_auth.sql:ro
86
- - ./node_modules/dzql/src/database/migrations/007_events.sql:/docker-entrypoint-initdb.d/007_events.sql:ro
87
- - ./node_modules/dzql/src/database/migrations/008_hello.sql:/docker-entrypoint-initdb.d/008_hello.sql:ro
88
- - ./node_modules/dzql/src/database/migrations/008a_meta.sql:/docker-entrypoint-initdb.d/008a_meta.sql:ro
89
- - ./init_db/001_domain.sql:/docker-entrypoint-initdb.d/010_domain.sql:ro
90
- ports:
91
- - "5432:5432"
92
- healthcheck:
93
- test: ["CMD-SHELL", "pg_isready -U dzql"]
94
- interval: 10s
95
- timeout: 5s
96
- retries: 5
97
- ```
98
-
99
- **For monorepo projects** (like the venues example), use relative paths:
100
- ```yaml
101
- volumes:
102
- - ../../dzql/src/database/migrations/001_schema.sql:/docker-entrypoint-initdb.d/001_schema.sql:ro
103
- # ... etc
104
- ```
105
-
106
- Start PostgreSQL:
107
- ```bash
108
- docker compose up -d
109
- ```
110
-
111
- ### 4. Database Schema
112
-
113
- Create `init_db/001_domain.sql`:
114
-
115
- ```sql
116
- CREATE TABLE IF NOT EXISTS todos (
117
- id SERIAL PRIMARY KEY,
118
- user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
119
- title TEXT NOT NULL,
120
- description TEXT,
121
- completed BOOLEAN DEFAULT FALSE,
122
- created_at TIMESTAMPTZ DEFAULT NOW(),
123
- updated_at TIMESTAMPTZ DEFAULT NOW()
124
- );
125
-
126
- CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos(user_id);
127
-
128
- SELECT dzql.register_entity(
129
- p_table_name := 'todos',
130
- p_label_field := 'title',
131
- p_searchable_fields := array['title', 'description']
132
- );
133
- ```
134
-
135
- ### 5. Server
136
-
137
- Create `index.js`:
138
-
139
- ```javascript
140
- import { createServer } from "dzql";
141
- import index from "./public/index.html";
142
-
143
- const server = createServer({
144
- port: process.env.PORT || 3000,
145
- routes: {
146
- "/": index,
147
- },
148
- });
149
-
150
- console.log(`🚀 DZQL Server running on http://localhost:${server.port}`);
151
- ```
152
-
153
- ### 6. HTML
154
-
155
- Create `public/index.html`:
156
-
157
- ```html
158
- <!doctype html>
159
- <html lang="en">
160
- <head>
161
- <meta charset="UTF-8" />
162
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
163
- <title>DZQL Todo App</title>
164
- <link rel="stylesheet" href="./index.css" />
165
- </head>
166
- <body>
167
- <div class="container">
168
- <h1>✓ Todo App</h1>
169
- <div id="status" class="status disconnected">Connecting...</div>
170
- <div id="error" class="error hidden"></div>
171
-
172
- <div id="authSection" class="section">
173
- <h2>Login or Register</h2>
174
- <form id="authForm" onsubmit="handleLoginSubmit(event)">
175
- <div class="form-group">
176
- <label>Email</label>
177
- <input type="email" id="email" placeholder="user@example.com" required />
178
- </div>
179
- <div class="form-group">
180
- <label>Password</label>
181
- <input type="password" id="password" placeholder="••••••••" required />
182
- </div>
183
- <div class="btn-group">
184
- <button type="submit" class="btn-primary">Login</button>
185
- <button type="button" class="btn-secondary" onclick="handleRegister()">Register</button>
186
- </div>
187
- </form>
188
- </div>
189
-
190
- <div id="appSection" class="section hidden">
191
- <button class="btn-secondary" style="width: 100%; margin-bottom: 20px" onclick="handleLogout()">
192
- Logout
193
- </button>
194
-
195
- <div class="section">
196
- <h2>Create Todo</h2>
197
- <form id="todoForm" onsubmit="event.preventDefault(); handleAddTodo()">
198
- <div class="form-group">
199
- <label>Title</label>
200
- <input type="text" id="todoTitle" placeholder="What needs to be done?" required />
201
- </div>
202
- <div class="form-group">
203
- <label>Description</label>
204
- <textarea id="todoDescription" placeholder="Add details..." rows="2"></textarea>
205
- </div>
206
- <button type="submit" class="btn-primary" style="width: 100%">Add Todo</button>
207
- </form>
208
- </div>
209
-
210
- <div class="section">
211
- <h2>Todos</h2>
212
- <div id="todoList" class="todo-list"></div>
213
- <div id="emptyState" class="empty-state">No todos yet</div>
214
- </div>
215
- </div>
216
- </div>
217
-
218
- <script type="module" src="./app.js"></script>
219
- </body>
220
- </html>
221
- ```
222
-
223
- ### 7. CSS
224
-
225
- Create `public/index.css`:
226
-
227
- ```css
228
- * {
229
- box-sizing: border-box;
230
- margin: 0;
231
- padding: 0;
232
- }
233
-
234
- body {
235
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
236
- background: #f5f5f5;
237
- padding: 20px;
238
- }
239
-
240
- .container {
241
- max-width: 600px;
242
- margin: 0 auto;
243
- background: white;
244
- padding: 30px;
245
- border-radius: 8px;
246
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
247
- }
248
-
249
- h1 {
250
- margin-bottom: 20px;
251
- color: #333;
252
- }
253
-
254
- .status {
255
- padding: 10px;
256
- border-radius: 4px;
257
- margin-bottom: 20px;
258
- font-weight: 500;
259
- }
260
-
261
- .status.connected {
262
- background: #d4edda;
263
- color: #155724;
264
- }
265
-
266
- .status.disconnected {
267
- background: #f8d7da;
268
- color: #721c24;
269
- }
270
-
271
- .error {
272
- padding: 10px;
273
- background: #f8d7da;
274
- color: #721c24;
275
- border-radius: 4px;
276
- margin-bottom: 20px;
277
- }
278
-
279
- .hidden {
280
- display: none;
281
- }
282
-
283
- .section {
284
- margin-bottom: 30px;
285
- }
286
-
287
- .form-group {
288
- margin-bottom: 15px;
289
- }
290
-
291
- .form-group label {
292
- display: block;
293
- margin-bottom: 5px;
294
- font-weight: 500;
295
- color: #333;
296
- }
297
-
298
- .form-group input,
299
- .form-group textarea {
300
- width: 100%;
301
- padding: 10px;
302
- border: 1px solid #ddd;
303
- border-radius: 4px;
304
- font-size: 14px;
305
- }
306
-
307
- .btn-group {
308
- display: flex;
309
- gap: 10px;
310
- }
311
-
312
- button {
313
- padding: 10px 20px;
314
- border: none;
315
- border-radius: 4px;
316
- cursor: pointer;
317
- font-size: 14px;
318
- font-weight: 500;
319
- }
320
-
321
- .btn-primary {
322
- background: #007bff;
323
- color: white;
324
- flex: 1;
325
- }
326
-
327
- .btn-primary:hover {
328
- background: #0056b3;
329
- }
330
-
331
- .btn-secondary {
332
- background: #6c757d;
333
- color: white;
334
- flex: 1;
335
- }
336
-
337
- .btn-secondary:hover {
338
- background: #545b62;
339
- }
340
-
341
- .btn-danger {
342
- background: #dc3545;
343
- color: white;
344
- padding: 5px 10px;
345
- font-size: 12px;
346
- }
347
-
348
- .btn-danger:hover {
349
- background: #c82333;
350
- }
351
-
352
- .todo-list {
353
- display: flex;
354
- flex-direction: column;
355
- gap: 10px;
356
- }
357
-
358
- .todo-item {
359
- display: flex;
360
- align-items: center;
361
- gap: 10px;
362
- padding: 15px;
363
- background: #f8f9fa;
364
- border-radius: 4px;
365
- border: 1px solid #e9ecef;
366
- }
367
-
368
- .todo-item.completed {
369
- opacity: 0.6;
370
- }
371
-
372
- .todo-item.completed .todo-title {
373
- text-decoration: line-through;
374
- }
375
-
376
- .todo-checkbox {
377
- width: 20px;
378
- height: 20px;
379
- cursor: pointer;
380
- }
381
-
382
- .todo-content {
383
- flex: 1;
384
- }
385
-
386
- .todo-title {
387
- font-weight: 500;
388
- margin-bottom: 5px;
389
- }
390
-
391
- .todo-description {
392
- font-size: 14px;
393
- color: #666;
394
- }
395
-
396
- .empty-state {
397
- text-align: center;
398
- color: #999;
399
- padding: 40px;
400
- font-style: italic;
401
- }
402
- ```
403
-
404
- ### 8. JavaScript
405
-
406
- Create `public/app.js`:
407
-
408
- ```javascript
409
- import { WebSocketManager } from "dzql/client";
410
-
411
- let ws = null;
412
- let state = {
413
- connected: false,
414
- loggedIn: false,
415
- userId: null,
416
- todos: [],
417
- };
418
-
419
- function query(selector) {
420
- return document.getElementById(selector);
421
- }
422
-
423
- function render() {
424
- const status = query("status");
425
- status.textContent = state.connected
426
- ? state.loggedIn
427
- ? "✓ Connected"
428
- : "✓ Connected (not logged in)"
429
- : "✗ Disconnected";
430
- status.className = `status ${state.connected ? "connected" : "disconnected"}`;
431
-
432
- query("authSection").classList.toggle("hidden", state.loggedIn);
433
- query("appSection").classList.toggle("hidden", !state.loggedIn);
434
-
435
- if (state.loggedIn) {
436
- const list = query("todoList");
437
- const empty = query("emptyState");
438
-
439
- if (state.todos.length === 0) {
440
- list.innerHTML = "";
441
- empty.classList.remove("hidden");
442
- } else {
443
- empty.classList.add("hidden");
444
- list.innerHTML = state.todos
445
- .map(
446
- (todo) => `
447
- <div class="todo-item ${todo.completed ? "completed" : ""}">
448
- <input type="checkbox" class="todo-checkbox" ${todo.completed ? "checked" : ""}
449
- onchange="handleToggleTodo(${todo.id})">
450
- <div class="todo-content">
451
- <div class="todo-title">${escapeHtml(todo.title)}</div>
452
- ${todo.description ? `<div class="todo-description">${escapeHtml(todo.description)}</div>` : ""}
453
- </div>
454
- <button class="btn-danger" onclick="handleDeleteTodo(${todo.id})">Delete</button>
455
- </div>
456
- `,
457
- )
458
- .join("");
459
- }
460
- }
461
- }
462
-
463
- function escapeHtml(text) {
464
- const div = document.createElement("div");
465
- div.textContent = text;
466
- return div.innerHTML;
467
- }
468
-
469
- function error(msg) {
470
- const el = query("error");
471
- el.textContent = msg;
472
- el.classList.remove("hidden");
473
- setTimeout(() => el.classList.add("hidden"), 5000);
474
- }
475
-
476
- async function auth(type) {
477
- const email = query("email").value;
478
- const password = query("password").value;
479
-
480
- if (!email || !password) {
481
- error("Email and password required");
482
- return;
483
- }
484
-
485
- try {
486
- const result = await ws.api[type]({ email, password });
487
- if (result.token) {
488
- localStorage.setItem("dzql_token", result.token);
489
- state.loggedIn = true;
490
- state.userId = result.user_id;
491
- await loadTodos();
492
- render();
493
- }
494
- } catch (err) {
495
- error(err.message || `${type} failed`);
496
- }
497
- }
498
-
499
- async function loadTodos() {
500
- try {
501
- const result = await ws.api.search.todos({
502
- filters: { user_id: state.userId },
503
- limit: 100,
504
- });
505
- state.todos = result.data || [];
506
- } catch (err) {
507
- error("Failed to load todos");
508
- throw err;
509
- }
510
- }
511
-
512
- async function handleAddTodo() {
513
- const title = query("todoTitle").value.trim();
514
- const description = query("todoDescription").value.trim();
515
-
516
- if (!title) {
517
- error("Title required");
518
- return;
519
- }
520
-
521
- try {
522
- await ws.api.save.todos({
523
- user_id: state.userId,
524
- title,
525
- description,
526
- completed: false,
527
- });
528
- query("todoTitle").value = "";
529
- query("todoDescription").value = "";
530
- } catch (err) {
531
- error("Failed to create todo");
532
- }
533
- }
534
-
535
- async function handleToggleTodo(id) {
536
- try {
537
- const todo = state.todos.find((t) => t.id === id);
538
- if (todo) {
539
- await ws.api.save.todos({
540
- id,
541
- user_id: state.userId,
542
- completed: !todo.completed,
543
- });
544
- }
545
- } catch (err) {
546
- error("Failed to update todo");
547
- }
548
- }
549
-
550
- async function handleDeleteTodo(id) {
551
- if (!confirm("Delete this todo?")) return;
552
- try {
553
- await ws.api.delete.todos({ id });
554
- } catch (err) {
555
- error("Failed to delete todo");
556
- }
557
- }
558
-
559
- async function init() {
560
- try {
561
- ws = new WebSocketManager();
562
- await ws.connect();
563
- state.connected = true;
564
-
565
- ws.onBroadcast((method, params) => {
566
- if (!state.loggedIn) return;
567
-
568
- const data = params.data;
569
- if (method === "todos:insert") {
570
- state.todos.push(data);
571
- } else if (method === "todos:update") {
572
- const idx = state.todos.findIndex((t) => t.id === data.id);
573
- if (idx !== -1) state.todos[idx] = data;
574
- } else if (method === "todos:delete") {
575
- state.todos = state.todos.filter((t) => t.id !== data.id);
576
- }
577
- render();
578
- });
579
-
580
- const token = localStorage.getItem("dzql_token");
581
- if (token) {
582
- try {
583
- const payload = JSON.parse(atob(token.split(".")[1]));
584
- state.userId = payload.user_id;
585
- await loadTodos();
586
- state.loggedIn = true;
587
- } catch (err) {
588
- localStorage.removeItem("dzql_token");
589
- state.loggedIn = false;
590
- state.userId = null;
591
- }
592
- }
593
-
594
- render();
595
- } catch (err) {
596
- state.connected = false;
597
- error("Failed to connect");
598
- render();
599
- }
600
- }
601
-
602
- window.handleAddTodo = handleAddTodo;
603
- window.handleToggleTodo = handleToggleTodo;
604
- window.handleDeleteTodo = handleDeleteTodo;
605
- window.handleLoginSubmit = (e) => {
606
- e.preventDefault();
607
- auth("login_user");
608
- };
609
- window.handleRegister = () => auth("register_user");
610
- window.handleLogout = async () => {
611
- try {
612
- await ws.api.logout();
613
- localStorage.removeItem("dzql_token");
614
- state.loggedIn = false;
615
- state.userId = null;
616
- state.todos = [];
617
- render();
618
- } catch (err) {
619
- error("Logout failed");
620
- }
621
- };
622
-
623
- init();
624
- ```
625
-
626
- ### 9. Package.json
627
-
628
- ```json
629
- {
630
- "name": "dzql-todo-app",
631
- "module": "index.js",
632
- "type": "module",
633
- "private": true,
634
- "scripts": {
635
- "dev": "bun --hot index.js",
636
- "start": "bun index.js",
637
- "db:up": "docker compose up -d",
638
- "db:down": "docker compose down",
639
- "test": "playwright test"
640
- },
641
- "dependencies": {
642
- "dzql": "latest"
643
- },
644
- "devDependencies": {
645
- "@playwright/test": "^1.56.1",
646
- "@types/bun": "latest"
647
- }
648
- }
649
- ```
650
-
651
- ## Run
652
-
653
- ```bash
654
- # Start database
655
- bun run db:up
656
-
657
- # Start server (with hot reload)
658
- bun run dev
659
-
660
- # In another terminal, run tests (optional)
661
- bun run test
662
- ```
663
-
664
- Access at `http://localhost:3000`
665
-
666
- ## Testing with Playwright
667
-
668
- Install Playwright:
669
- ```bash
670
- bun add -d @playwright/test
671
- bunx playwright install
672
- ```
673
-
674
- Create `app.test.ts`:
675
- ```typescript
676
- import { test, expect } from "@playwright/test";
677
-
678
- test("register and create todo", async ({ page }) => {
679
- await page.goto("http://localhost:3000");
680
- await page.waitForLoadState("networkidle");
681
-
682
- // Register
683
- await page.locator("#email").fill(`test-${Date.now()}@example.com`);
684
- await page.locator("#password").fill("testpass123");
685
- await page.locator("button:has-text('Register')").click();
686
- await page.waitForLoadState("networkidle");
687
-
688
- // Create todo
689
- await page.locator("#todoTitle").fill("Buy milk");
690
- await page.locator("button:has-text('Add Todo')").click();
691
- await expect(page.locator(".todo-title:has-text('Buy milk')")).toBeVisible();
692
- });
693
- ```
694
-
695
- Run tests:
696
- ```bash
697
- bunx playwright test --headed # see browser
698
- bunx playwright test # headless
699
- ```
700
-
701
- **Testing Tips:**
702
- - Use unique emails per test: `test-${Date.now()}@example.com`
703
- - Wait for network: `await page.waitForLoadState("networkidle")`
704
- - Find elements by id, class, or text: `locator("#id")`, `locator(".class")`, `locator("button:has-text('text')")`
705
-
706
- ## Key Architecture
707
-
708
- ### Atomic Updates & Real-time Sync
709
-
710
- When user A saves a todo, it:
711
- 1. Updates database atomically
712
- 2. Broadcasts to all connected clients instantly
713
- 3. All users see the change immediately
714
-
715
- ```javascript
716
- ws.onBroadcast((method, params) => {
717
- const data = params.data;
718
- if (method === "todos:insert") {
719
- state.todos.push(data); // Add new
720
- } else if (method === "todos:update") {
721
- const idx = state.todos.findIndex(t => t.id === data.id);
722
- if (idx !== -1) state.todos[idx] = data; // Replace
723
- } else if (method === "todos:delete") {
724
- state.todos = state.todos.filter(t => t.id !== data.id); // Remove
725
- }
726
- render(); // Re-render UI with new state
727
- });
728
- ```
729
-
730
- That's it. Every user creating/updating/deleting a todo sees it **instantly** on all other clients. No polling, no re-fetching, no race conditions.
731
-
732
- ### State Management
733
- - `state.connected` - WebSocket status
734
- - `state.loggedIn` - Authentication status
735
- - `state.userId` - Current user (from JWT on reload)
736
- - `state.todos` - User's todos array
737
-
738
- ### Authentication Flow
739
- 1. User registers/logs in
740
- 2. Server returns token + user_id
741
- 3. Token stored in localStorage
742
- 4. On page reload, JWT decoded to get user_id
743
- 5. Todos loaded with user filter
744
-
745
- ### Single Render Function
746
- - `render()` handles ALL UI updates
747
- - Called after every state change
748
- - Also called on every broadcast update
749
- - No separate update functions needed
750
-
751
- ## The 5 DZQL Operations
752
-
753
- Every registered entity automatically gets these 5 operations:
754
-
755
- ### 1. GET - Single Record
756
- ```javascript
757
- const record = await ws.api.get.tableName({ id: 1 });
758
- // Throws "record not found" error if not exists
759
- ```
760
-
761
- ### 2. SAVE - Upsert
762
- ```javascript
763
- const record = await ws.api.save.tableName({
764
- id: 1, // Optional - omit for insert
765
- name: 'Updated'
766
- });
767
- ```
768
-
769
- ### 3. DELETE - Remove Record
770
- ```javascript
771
- const deleted = await ws.api.delete.tableName({ id: 1 });
772
- ```
773
-
774
- ### 4. LOOKUP - Autocomplete
775
- ```javascript
776
- const options = await ws.api.lookup.tableName({
777
- p_filter: 'search term'
778
- });
779
- // Returns: [{ label: 'Display Name', value: 1 }, ...]
780
- ```
781
-
782
- ### 5. SEARCH - Advanced Search
783
- ```javascript
784
- const results = await ws.api.search.tableName({
785
- filters: {
786
- name: { ilike: '%john%' },
787
- age: { gte: 18 },
788
- city: ['NYC', 'LA'],
789
- active: true
790
- },
791
- sort: { field: 'name', order: 'asc' },
792
- page: 1,
793
- limit: 25
794
- });
795
- // Returns: { data: [...], total: 100, page: 1, limit: 25 }
796
- ```
797
-
798
- ## Entity Registration
799
-
800
- Before DZQL can work with a table, you must register it with `dzql.register_entity()`.
801
-
802
- **Important:** This function is provided by DZQL's core migrations, which run automatically when PostgreSQL starts via compose.yml.
803
-
804
- ```sql
805
- SELECT dzql.register_entity(
806
- p_table_name := 'venues', -- Your table name
807
- p_label_field := 'name', -- Used by LOOKUP
808
- p_searchable_fields := array['name', 'address'], -- Used by SEARCH
809
- p_fk_includes := '{"org": "organisations"}'::jsonb, -- Dereference FKs
810
- p_graph_rules := '{}'::jsonb -- Optional: automation
811
- );
812
- ```
813
-
814
- **Parameters:**
815
- - `p_table_name`: Your PostgreSQL table name
816
- - `p_label_field`: Which field to use for display (LOOKUP)
817
- - `p_searchable_fields`: Fields searchable by SEARCH
818
- - `p_fk_includes`: Foreign keys to auto-dereference (optional)
819
- - `p_graph_rules`: Graph rules for automation (optional)
820
-
821
- See the [venues example](https://github.com/blueshed/dzql/blob/main/packages/venues/database/init_db/009_venues_domain.sql) for a complete schema with multiple entity registrations.
822
-
823
- ## Custom API Functions
824
-
825
- Add custom functions that work alongside DZQL operations.
826
-
827
- ### PostgreSQL Functions
828
-
829
- Create stored procedures that execute server-side:
830
-
831
- ```sql
832
- CREATE OR REPLACE FUNCTION my_function(
833
- p_user_id INT,
834
- p_name TEXT DEFAULT 'World'
835
- ) RETURNS JSONB
836
- LANGUAGE plpgsql
837
- SECURITY DEFINER
838
- AS $$
839
- BEGIN
840
- -- Your logic here
841
- RETURN jsonb_build_object('message', 'Hello, ' || p_name);
842
- END;
843
- $$;
844
- ```
845
-
846
- Call from client:
847
- ```javascript
848
- const result = await ws.api.my_function({ name: 'World' });
849
- // Returns: { message: 'Hello, World' }
850
- ```
851
-
852
- ### Bun Functions
853
-
854
- Create JavaScript functions that run in the Bun server:
855
-
856
- ```javascript
857
- // server/api.js
858
- export async function myBunFunction(userId, params = {}) {
859
- const { name = 'World' } = params;
860
-
861
- // Can use db.api for database access
862
- // const user = await db.api.get.users({ id: userId }, userId);
863
-
864
- return {
865
- message: `Hello, ${name}!`,
866
- user_id: userId
867
- };
868
- }
869
- ```
870
-
871
- Pass to server:
872
- ```javascript
873
- import { createServer } from 'dzql';
874
- import * as customApi from './server/api.js';
875
-
876
- const server = createServer({
877
- port: 3000,
878
- customApi
879
- });
880
- ```
881
-
882
- Call from client:
883
- ```javascript
884
- const result = await ws.api.myBunFunction({ name: 'World' });
885
- ```
886
-
887
- **Both types:**
888
- - First parameter is always `user_id` (auto-injected on client)
889
- - Require authentication
890
- - Use same proxy API syntax
891
- - Return JSON-serializable data
892
-
893
- ### Advanced Server Setup
894
-
895
- For routes that need real-time broadcasting:
896
-
897
- ```javascript
898
- import { createServer } from 'dzql';
899
-
900
- const server = createServer({
901
- port: 3000,
902
-
903
- // Static routes (don't need broadcast)
904
- routes: {
905
- '/health': () => new Response('OK'),
906
- '/': () => new Response('Hello')
907
- },
908
-
909
- // Routes that need broadcasting
910
- onReady: async (broadcast) => {
911
- // broadcast(method, params) - send to all clients
912
-
913
- return {
914
- '/custom': async (req) => {
915
- // Your logic
916
- broadcast('custom:event', { data: 'something' });
917
- return new Response('Done');
918
- }
919
- };
920
- }
921
- });
922
- ```
923
-
924
- ## Real-time Events
925
-
926
- DZQL broadcasts changes in real-time via WebSocket:
927
-
928
- ```javascript
929
- // Listen for all events
930
- const unsubscribe = ws.onBroadcast((method, params) => {
931
- console.log(`Event: ${method}`, params);
932
- // method: "users:insert", "users:update", "users:delete"
933
- // params: { table, op, pk, before, after, user_id, at }
934
- });
935
-
936
- // Stop listening
937
- unsubscribe();
938
- ```
939
-
940
- Event format:
941
- - `method`: `"{table}:{operation}"` (e.g., "todos:insert")
942
- - `params.data`: The affected record
943
- - `params.user_id`: User who made the change
944
- - `params.at`: Timestamp
945
-
946
- ## Authentication
947
-
948
- DZQL provides built-in user authentication:
949
-
950
- ```javascript
951
- // Register
952
- const result = await ws.api.register_user({
953
- email: 'user@example.com',
954
- password: 'secure-password'
955
- });
956
-
957
- // Login
958
- const result = await ws.api.login_user({
959
- email: 'user@example.com',
960
- password: 'secure-password'
961
- });
962
- // Returns: { token, profile, user_id }
963
-
964
- // Logout
965
- await ws.api.logout();
966
-
967
- // Save token for auto-login
968
- localStorage.setItem('dzql_token', result.token);
969
-
970
- // Auto-connect with token
971
- const ws = new WebSocketManager();
972
- await ws.connect(); // Automatically uses token from localStorage
973
- ```
974
-
975
- ## Server-Side API Usage
976
-
977
- For backend/Bun scripts:
978
-
979
- ```javascript
980
- import { db, sql } from 'dzql';
981
-
982
- // Direct SQL queries
983
- const users = await sql`SELECT * FROM users`;
984
-
985
- // DZQL operations (require explicit userId)
986
- const user = await db.api.get.users({ id: 1 }, userId);
987
- const saved = await db.api.save.users({ name: 'John' }, userId);
988
- const results = await db.api.search.users({ filters: {} }, userId);
989
- ```
990
-
991
- **Key difference:** Server-side requires explicit `userId` as second parameter; client-side auto-injects from JWT.
992
-
993
- ## Environment Variables
994
-
995
- ```bash
996
- # Server
997
- PORT=3000
998
- DATABASE_URL=postgresql://dzql:dzql@localhost:5432/dzql
999
- NODE_ENV=development
1000
- LOG_LEVEL=INFO
1001
-
1002
- # JWT
1003
- JWT_SECRET=your-secret-key
1004
- JWT_EXPIRES_IN=7d
1005
-
1006
- # WebSocket
1007
- WS_PING_INTERVAL=30000
1008
- WS_PING_TIMEOUT=5000
1009
- ```
1010
-
1011
- ## Error Handling
1012
-
1013
- ```javascript
1014
- try {
1015
- const user = await ws.api.get.users({ id: 999 });
1016
- } catch (error) {
1017
- console.error(error.message);
1018
- // "record not found" - record doesn't exist
1019
- // "Permission denied: view on users" - access denied
1020
- // "Function not found" - custom function doesn't exist
1021
- }
1022
- ```
1023
-
1024
- ## Troubleshooting
1025
-
1026
- ### Database won't connect
1027
- ```bash
1028
- # Check if PostgreSQL is running
1029
- docker ps
1030
-
1031
- # Check logs
1032
- docker compose logs postgres
1033
-
1034
- # Restart database (fresh start)
1035
- docker compose down -v && docker compose up -d
1036
- ```
1037
-
1038
- ### WebSocket connection fails
1039
- - Ensure server is running: `http://localhost:3000`
1040
- - Check firewall for port 3000
1041
- - Check browser console for errors
1042
- - Verify WebSocket URL in client code
1043
-
1044
- ### Entity not found errors
1045
- - Verify table is created in database
1046
- - Verify entity is registered with `dzql.register_entity()`
1047
- - Check `p_table_name` matches table name exactly
1048
- - Check migrations ran: `docker compose logs postgres`
1049
-
1050
- ### Permission denied errors
1051
- - Implement permission rules in your entity registration
1052
- - Check user authentication status
1053
- - Verify user_id is being passed correctly
1054
- - See venues example for permission path syntax
1055
-
1056
- ### Migrations not running
1057
- - Check volume mounts in compose.yml
1058
- - Ensure migration files exist in node_modules/dzql/
1059
- - Check PostgreSQL logs: `docker compose logs postgres`
1060
- - Try fresh start: `docker compose down -v && docker compose up -d`
1061
-
1062
- ### Real-time updates not working
1063
- - Verify `onBroadcast` handler is set up
1064
- - Check WebSocket connection status
1065
- - Check browser console for errors
1066
- - Verify entity is registered (triggers events)
1067
-
1068
- ## DRY Principles Used
1069
-
1070
- - **Single `auth()` function** handles both login and register
1071
- - **Single `render()` function** handles all UI updates
1072
- - **`query()` helper** replaces repeated `getElementById()`
1073
- - **Broadcast handler** calls `render()` once instead of separate functions
1074
- - **Combined error handling** with reusable `error()` function
1075
-
1076
- ## Key Takeaways for Claude
1077
-
1078
- When building with DZQL:
1079
- 1. **Schema first** - Table definition automatically gives you API
1080
- 2. **Broadcasts = Real-time** - Listen to `entity:insert/update/delete` events
1081
- 3. **One render function** - Update state, call render(), DOM updates
1082
- 4. **User ID filtering** - Always pass `user_id` in queries and mutations
1083
- 5. **Token from JWT** - Decode on init: `JSON.parse(atob(token.split(".")[1]))`
1084
- 6. **DRY helpers** - Use `query()` for DOM, combine auth logic, single error handler
1085
- 7. **Index pattern** - `const idx = state.todos.findIndex(t => t.id === data.id)` for updates
1086
-
1087
- The app should feel simple. If it feels complex, something is wrong.
1088
-
1089
- ## Next Steps
1090
-
1091
- 1. **Read the full documentation**: Check the framework's README and CLAUDE.md
1092
- 2. **Explore graph rules**: Add automation with `p_graph_rules`
1093
- 3. **Implement permissions**: Use path-based access control
1094
- 4. **Add notifications**: Set up `p_notification_path` for real-time updates
1095
- 5. **Build your UI**: Connect to any frontend framework (React, Vue, Svelte, etc.)
1096
- 6. **Study the venues example**: See a complete multi-entity application
1097
-
1098
- ## Support
1099
-
1100
- - **GitHub**: https://github.com/blueshed/dzql
1101
- - **Issues**: https://github.com/blueshed/dzql/issues
1102
- - **Documentation**: See README.md and CLAUDE.md in the package
1103
-
1104
- Happy building! 🚀