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,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! 🚀
|