dzql 0.6.18 → 0.6.20
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/docs/README.md +71 -321
- package/docs/for_ai.md +355 -552
- package/package.json +1 -1
- package/src/cli/codegen/sql.ts +26 -0
- package/src/cli/codegen/types.ts +8 -4
package/docs/README.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
# DZQL:
|
|
1
|
+
# DZQL: Realtime Apps Without the Boilerplate
|
|
2
2
|
|
|
3
|
-
DZQL
|
|
3
|
+
DZQL compiles your data model into a complete realtime backend. Define entities and permissions in TypeScript, get a PostgreSQL database with CRUD operations, WebSocket sync, and type-safe client SDK - all generated.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why DZQL?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Building realtime apps typically means:
|
|
8
|
+
- Writing CRUD endpoints for every entity
|
|
9
|
+
- Implementing row-level security in your API layer
|
|
10
|
+
- Keeping frontend state in sync with the database
|
|
11
|
+
- Managing WebSocket subscriptions manually
|
|
12
|
+
|
|
13
|
+
**DZQL eliminates this.** You define *what* your data looks like. DZQL generates *how* it works.
|
|
14
|
+
|
|
15
|
+
## 30-Second Start
|
|
8
16
|
|
|
9
17
|
```bash
|
|
10
18
|
bun create dzql my-app
|
|
@@ -14,357 +22,99 @@ bun run db:rebuild
|
|
|
14
22
|
bun run dev
|
|
15
23
|
```
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## The Problem
|
|
20
|
-
|
|
21
|
-
Building realtime apps is hard. You typically have to:
|
|
22
|
-
1. **Sync State:** Manually keep your frontend Pinia/Redux store in sync with your backend database.
|
|
23
|
-
2. **Manage Permissions:** Re-implement row-level security in your API layer (and hope it matches your DB).
|
|
24
|
-
3. **Handle Atomicity:** Ensure that complex operations (e.g., "Create Order + Reserve Inventory + Notify User") happen in a single transaction.
|
|
25
|
-
4. **Optimistic Updates:** Write complex client-side logic to "guess" the server's response, often leading to data divergence.
|
|
26
|
-
|
|
27
|
-
## The DZQL Solution
|
|
28
|
-
|
|
29
|
-
DZQL takes a radically different approach: **Compilation**.
|
|
30
|
-
|
|
31
|
-
Instead of a heavy runtime framework, you define your **Domain Schema** (Entities, Relationships, Permissions) in a simple TypeScript configuration. DZQL compiles this definition into:
|
|
25
|
+
Open http://localhost:5173 - you have a working realtime app.
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
2. **Type-Safe Client SDK:** A generated TypeScript client that knows your exact API surface.
|
|
35
|
-
3. **Smart Pinia Stores:** Generated Vue stores that automatically handle realtime synchronization using atomic "Patch Events" from the database.
|
|
27
|
+
## How It Works
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* **Zero Runtime Interpretation:** No slow ORM query builders. Everything is compiled to native PL/pgSQL.
|
|
40
|
-
* **Security by Construction:** The runtime is a "dumb" gateway that routes requests by OID allowlist. It *cannot* execute arbitrary SQL.
|
|
41
|
-
* **Atomic Everything:** Complex graph operations (cascading creates/deletes) happen in a single database transaction.
|
|
42
|
-
* **Realtime by Default:** Every database write emits an atomic event batch. The client SDK automatically patches your local state. No "refetching" required.
|
|
43
|
-
* **JavaScript/TypeScript Native:** Define your schema in code you understand, get full type safety end-to-end.
|
|
44
|
-
|
|
45
|
-
## Manual Setup
|
|
46
|
-
|
|
47
|
-
If you prefer to set up manually instead of using `bun create dzql`:
|
|
48
|
-
|
|
49
|
-
### 1. Define your Domain (`domain.ts`)
|
|
29
|
+
**1. Define your domain** (`domain.ts`):
|
|
50
30
|
|
|
51
31
|
```typescript
|
|
52
32
|
export const entities = {
|
|
53
33
|
posts: {
|
|
54
|
-
schema: {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
34
|
+
schema: {
|
|
35
|
+
id: 'serial PRIMARY KEY',
|
|
36
|
+
title: 'text NOT NULL',
|
|
37
|
+
author_id: 'int REFERENCES users(id)'
|
|
38
|
+
},
|
|
39
|
+
permissions: {
|
|
40
|
+
view: [], // Anyone can view
|
|
41
|
+
update: ['@author_id'] // Only author can edit
|
|
42
|
+
}
|
|
63
43
|
}
|
|
64
44
|
};
|
|
65
45
|
```
|
|
66
46
|
|
|
67
|
-
|
|
47
|
+
**2. Compile:**
|
|
68
48
|
|
|
69
49
|
```bash
|
|
70
|
-
bunx dzql domain.ts
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 3. Use in Client
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
import { usePostFeedStore } from '@generated/client/stores';
|
|
77
|
-
|
|
78
|
-
const feed = usePostFeedStore();
|
|
79
|
-
// Automatically fetches data AND subscribes to realtime updates
|
|
80
|
-
// bind() is async - awaits until first data arrives
|
|
81
|
-
const { data, loading } = await feed.bind({ user_id: 1 });
|
|
82
|
-
// data.value is now populated
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Architecture
|
|
86
|
-
|
|
87
|
-
* **Compiler:** CLI tool that analyzes your domain and generates artifacts.
|
|
88
|
-
* **Runtime:** A lightweight Bun/Node server that handles Auth (JWT) and WebSocket connection pooling.
|
|
89
|
-
* **Client:** A robust WebSocket SDK that manages reconnection and dispatches atomic patches to stores.
|
|
90
|
-
* **Namespace:** Direct database access for CLI tools like `invokej`.
|
|
91
|
-
|
|
92
|
-
## Package Exports
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
import { ... } from 'dzql'; // Runtime server
|
|
96
|
-
import { ... } from 'dzql/client'; // WebSocket client SDK
|
|
97
|
-
import { ... } from 'dzql/compiler'; // CLI compiler
|
|
98
|
-
import { DzqlNamespace } from 'dzql/namespace'; // CLI/invokej integration
|
|
50
|
+
bunx dzql domain.ts
|
|
99
51
|
```
|
|
100
52
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
### Connection Flow
|
|
106
|
-
|
|
107
|
-
1. Client connects with optional `?token=...` in URL
|
|
108
|
-
2. Server validates token (if present) and fetches user profile
|
|
109
|
-
3. Server sends: `{"method": "connection:ready", "params": {"user": {...} | null}}`
|
|
110
|
-
4. Client knows auth state immediately
|
|
111
|
-
|
|
112
|
-
### Client API
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { WebSocketManager } from 'dzql/client';
|
|
116
|
-
|
|
117
|
-
const ws = new WebSocketManager();
|
|
118
|
-
await ws.connect('ws://localhost:3000/ws');
|
|
119
|
-
|
|
120
|
-
// Check connection state
|
|
121
|
-
console.log(ws.ready); // true after connection:ready received
|
|
122
|
-
console.log(ws.user); // user profile object or null
|
|
123
|
-
|
|
124
|
-
// Register callback for ready state
|
|
125
|
-
ws.onReady((user) => {
|
|
126
|
-
if (user) {
|
|
127
|
-
console.log('Authenticated as:', user.email);
|
|
128
|
-
} else {
|
|
129
|
-
console.log('Anonymous connection');
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Authentication methods
|
|
134
|
-
await ws.login({ email: '...', password: '...' }); // Stores token in localStorage
|
|
135
|
-
await ws.logout(); // Clears token and user state
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Vue/Pinia Usage Pattern
|
|
139
|
-
|
|
140
|
-
```vue
|
|
141
|
-
<template>
|
|
142
|
-
<div v-if="!ws.ready">Loading...</div>
|
|
143
|
-
<LoginModal v-else-if="!ws.user" />
|
|
144
|
-
<RouterView v-else />
|
|
145
|
-
</template>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Generated Pinia Subscribable Stores
|
|
149
|
-
|
|
150
|
-
DZQL generates Pinia stores for each subscribable that handle:
|
|
151
|
-
- Initial data fetch via WebSocket subscription
|
|
152
|
-
- Automatic realtime patching when related data changes
|
|
153
|
-
- Deduplication of subscriptions by parameter key
|
|
154
|
-
|
|
155
|
-
### Store Structure
|
|
156
|
-
|
|
157
|
-
Each generated store exports:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
const store = useVenueDetailStore();
|
|
161
|
-
|
|
162
|
-
// Main API
|
|
163
|
-
store.bind(params) // Async - subscribes and returns { data, loading, ready }
|
|
164
|
-
store.unbind(params) // Unsubscribes and removes document from store
|
|
165
|
-
store.documents // Ref containing all bound documents keyed by JSON.stringify(params)
|
|
166
|
-
```
|
|
53
|
+
This generates:
|
|
54
|
+
- SQL migrations with CRUD functions and permission checks
|
|
55
|
+
- TypeScript client SDK with full type safety
|
|
56
|
+
- Pinia stores with automatic realtime sync
|
|
167
57
|
|
|
168
|
-
|
|
58
|
+
**3. Use in your app:**
|
|
169
59
|
|
|
170
60
|
```typescript
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const store = useVenueDetailStore();
|
|
174
|
-
|
|
175
|
-
// bind() is async - returns when first data arrives
|
|
176
|
-
const { data, loading, ready } = await store.bind({ venue_id: 1 });
|
|
177
|
-
|
|
178
|
-
// data is reactive and contains the document
|
|
179
|
-
console.log(data); // { id: 1, name: 'My Venue', sites: [...], org: {...} }
|
|
61
|
+
// Type-safe API
|
|
62
|
+
const post = await ws.api.save_posts({ title: 'Hello World' });
|
|
180
63
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
const same = await store.bind({ venue_id: 1 }); // Returns immediately, no new subscription
|
|
64
|
+
// Realtime subscriptions
|
|
65
|
+
const store = usePostFeedStore();
|
|
66
|
+
const { data } = await store.bind({ author_id: 1 });
|
|
67
|
+
// data updates automatically when posts change - no refetching
|
|
186
68
|
```
|
|
187
69
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
**Pattern 1: Top-level await (recommended with `<Suspense>`)**
|
|
70
|
+
## Key Concepts
|
|
191
71
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
72
|
+
| Concept | What it does |
|
|
73
|
+
|---------|--------------|
|
|
74
|
+
| **Entities** | Database tables with CRUD operations (get, save, delete, search, lookup) |
|
|
75
|
+
| **Get = Rich Document** | `get` returns FK expansions and M2M relationships - a complete document |
|
|
76
|
+
| **Subscribables** | For complex queries with realtime sync (one-to-many, nested includes) |
|
|
77
|
+
| **Permissions** | Row-level security compiled to SQL |
|
|
78
|
+
| **Graph Rules** | Side effects on create/update/delete |
|
|
195
79
|
|
|
196
|
-
|
|
197
|
-
const store = useVenueDetailStore();
|
|
80
|
+
**Progression:** Start with `get` for simple documents. Move to subscribables when you need one-to-many relationships or realtime updates across multiple tables.
|
|
198
81
|
|
|
199
|
-
|
|
200
|
-
const { data } = await store.bind({ venue_id: props.venueId });
|
|
201
|
-
</script>
|
|
82
|
+
## What Gets Generated
|
|
202
83
|
|
|
203
|
-
<template>
|
|
204
|
-
<!-- data is guaranteed to be populated -->
|
|
205
|
-
<h1>{{ data.name }}</h1>
|
|
206
|
-
<p>Organization: {{ data.org.name }}</p>
|
|
207
|
-
<ul>
|
|
208
|
-
<li v-for="site in data.sites" :key="site.id">
|
|
209
|
-
{{ site.name }} ({{ site.allocations.length }} allocations)
|
|
210
|
-
</li>
|
|
211
|
-
</ul>
|
|
212
|
-
</template>
|
|
213
84
|
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
import { ref, onMounted, watch } from 'vue';
|
|
221
|
-
|
|
222
|
-
const props = defineProps(['venueId']);
|
|
223
|
-
const store = useVenueDetailStore();
|
|
224
|
-
const docState = ref({ data: null, loading: true });
|
|
225
|
-
|
|
226
|
-
onMounted(async () => {
|
|
227
|
-
docState.value = await store.bind({ venue_id: props.venueId });
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// Re-bind when venueId changes
|
|
231
|
-
watch(() => props.venueId, async (newId) => {
|
|
232
|
-
docState.value = await store.bind({ venue_id: newId });
|
|
233
|
-
});
|
|
234
|
-
</script>
|
|
235
|
-
|
|
236
|
-
<template>
|
|
237
|
-
<div v-if="docState.loading">Loading...</div>
|
|
238
|
-
<div v-else>
|
|
239
|
-
<h1>{{ docState.data.name }}</h1>
|
|
240
|
-
</div>
|
|
241
|
-
</template>
|
|
85
|
+
generated/
|
|
86
|
+
├── db/migrations/ # SQL schema + functions
|
|
87
|
+
├── runtime/manifest.json # API allowlist
|
|
88
|
+
└── client/
|
|
89
|
+
├── ws.ts # Type-safe WebSocket client
|
|
90
|
+
└── stores/ # Pinia stores with realtime sync
|
|
242
91
|
```
|
|
243
92
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
```vue
|
|
247
|
-
<script setup>
|
|
248
|
-
import { useVenueDetailStore } from '@/generated/client/stores';
|
|
249
|
-
|
|
250
|
-
const store = useVenueDetailStore();
|
|
251
|
-
|
|
252
|
-
// Bind multiple venues - each gets its own cached subscription
|
|
253
|
-
const venues = await Promise.all([
|
|
254
|
-
store.bind({ venue_id: 1 }),
|
|
255
|
-
store.bind({ venue_id: 2 }),
|
|
256
|
-
store.bind({ venue_id: 3 }),
|
|
257
|
-
]);
|
|
258
|
-
</script>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Accessing All Documents
|
|
262
|
-
|
|
263
|
-
The store's `documents` ref contains all bound subscriptions:
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
const store = useVenueDetailStore();
|
|
267
|
-
|
|
268
|
-
await store.bind({ venue_id: 1 });
|
|
269
|
-
await store.bind({ venue_id: 2 });
|
|
270
|
-
|
|
271
|
-
// Access all documents
|
|
272
|
-
console.log(store.documents);
|
|
273
|
-
// {
|
|
274
|
-
// '{"venue_id":1}': { data: {...}, loading: false, ready: Promise },
|
|
275
|
-
// '{"venue_id":2}': { data: {...}, loading: false, ready: Promise }
|
|
276
|
-
// }
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### How Realtime Works
|
|
280
|
-
|
|
281
|
-
DZQL uses a simple, unified broadcast pattern for all stores:
|
|
282
|
-
|
|
283
|
-
1. **Database events:** PostgreSQL triggers emit events to `dzql_v2.events`
|
|
284
|
-
2. **Server broadcasts:** Runtime sends `{table}:{op}` messages (e.g., `venues:update`) to clients based on:
|
|
285
|
-
- **Subscriptions:** Connections with matching `affected_keys`
|
|
286
|
-
- **Notifications:** Users in `notify_users` (from entity notification paths)
|
|
287
|
-
3. **Auto-dispatch:** The WebSocket client routes broadcasts to registered store handlers
|
|
288
|
-
4. **Store updates:** Each store's `table_changed` method applies updates to local data
|
|
289
|
-
5. **Vue reactivity:** UI updates automatically
|
|
290
|
-
|
|
291
|
-
**No refetching required** - changes are applied incrementally.
|
|
292
|
-
|
|
293
|
-
### The `table_changed` Pattern
|
|
294
|
-
|
|
295
|
-
Every generated store implements `table_changed` and self-registers with the WebSocket client:
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
// Generated store (simplified)
|
|
299
|
-
export const useVenuesStore = defineStore('venues-store', () => {
|
|
300
|
-
const records = ref([]);
|
|
301
|
-
|
|
302
|
-
function table_changed(table: string, op: string, pk: Record<string, unknown>, data: unknown) {
|
|
303
|
-
if (table !== 'venues') return;
|
|
304
|
-
// Update records based on op (insert/update/delete)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Self-register - no manual setup needed!
|
|
308
|
-
ws.registerStore(table_changed);
|
|
93
|
+
## Architecture
|
|
309
94
|
|
|
310
|
-
return { records, get, save, search, table_changed };
|
|
311
|
-
});
|
|
312
95
|
```
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
96
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
97
|
+
│ Browser │────▶│ Runtime │────▶│ PostgreSQL │
|
|
98
|
+
│ Vue + SDK │◀────│ WebSocket │◀────│ + Notify │
|
|
99
|
+
└─────────────┘ └─────────────┘ └─────────────┘
|
|
100
|
+
Pinia Allowlist Compiled
|
|
101
|
+
Stores Gateway SQL
|
|
319
102
|
```
|
|
320
103
|
|
|
321
|
-
|
|
104
|
+
- **Compiler**: Generates SQL and TypeScript at build time
|
|
105
|
+
- **Runtime**: Lightweight WebSocket gateway (no query building)
|
|
106
|
+
- **Client**: Type-safe SDK with automatic state sync
|
|
322
107
|
|
|
323
|
-
|
|
324
|
-
{
|
|
325
|
-
"jsonrpc": "2.0",
|
|
326
|
-
"method": "venues:update", // {table}:{op}
|
|
327
|
-
"params": {
|
|
328
|
-
"pk": { "id": 123 },
|
|
329
|
-
"data": { "id": 123, "name": "Updated Venue", ... }
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
```
|
|
108
|
+
## Learn More
|
|
333
109
|
|
|
334
|
-
|
|
110
|
+
- [Domain Modeling Guide](./for_ai.md) - Entity and permission patterns
|
|
111
|
+
- [Project Setup](./project-setup.md) - Manual setup and configuration
|
|
112
|
+
- [Feature Requests](./feature-requests/) - Roadmap and proposals
|
|
335
113
|
|
|
336
|
-
|
|
114
|
+
## Package Exports
|
|
337
115
|
|
|
338
116
|
```typescript
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
notifications: {
|
|
343
|
-
members: ['@org_id->acts_for[org_id=$]{active}.user_id']
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
};
|
|
117
|
+
import { startServer } from 'dzql'; // Runtime server
|
|
118
|
+
import { WebSocketManager } from 'dzql/client'; // Client SDK
|
|
119
|
+
import { DzqlNamespace } from 'dzql/namespace'; // CLI/scripting
|
|
347
120
|
```
|
|
348
|
-
|
|
349
|
-
When a venue is created/updated/deleted, all active members of that org receive the broadcast.
|
|
350
|
-
|
|
351
|
-
## Why "Compile-Only"?
|
|
352
|
-
|
|
353
|
-
By moving complexity to build-time, we get:
|
|
354
|
-
* **Performance:** The database does the heavy lifting.
|
|
355
|
-
* **Correctness:** If it compiles, your permissions and relationships are valid.
|
|
356
|
-
* **Simplicity:** The runtime is tiny and easy to audit.
|
|
357
|
-
|
|
358
|
-
## Entity Options
|
|
359
|
-
|
|
360
|
-
| Option | Type | Description |
|
|
361
|
-
|--------|------|-------------|
|
|
362
|
-
| `schema` | object | Column definitions with PostgreSQL types |
|
|
363
|
-
| `primaryKey` | string[] | Composite primary key fields (default: `['id']`) |
|
|
364
|
-
| `label` | string | Field used for display/autocomplete |
|
|
365
|
-
| `softDelete` | boolean | Use `deleted_at` instead of hard delete |
|
|
366
|
-
| `managed` | boolean | Set to `false` to skip CRUD generation (for junction tables) |
|
|
367
|
-
| `permissions` | object | Row-level security rules |
|
|
368
|
-
| `graphRules` | object | Side effects on create/update/delete |
|
|
369
|
-
| `manyToMany` | object | M2M relationship definitions |
|
|
370
|
-
| `fieldDefaults` | object | Default values for fields on INSERT |
|