dzql 0.6.19 → 0.6.21

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 CHANGED
@@ -1,10 +1,18 @@
1
- # DZQL: The Compile-Only Realtime Database Framework
1
+ # DZQL: Realtime Apps Without the Boilerplate
2
2
 
3
- DZQL ("Database Zero Query Language") is a PostgreSQL-native framework for building realtime, reactive applications without the runtime overhead or complexity of traditional ORMs or BaaS solutions.
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
- ## Quick Start
5
+ ## Why DZQL?
6
6
 
7
- The fastest way to get started is with `bun create`:
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
- This creates a full-stack app with Vue/Vite frontend, DZQL server, and PostgreSQL database.
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
- 1. **Optimized SQL:** Specialized PostgreSQL functions (`save_order`, `get_product`) with *inlined* permission checks and *atomic* graph operations.
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
- ### Key Features
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: { id: 'serial PRIMARY KEY', title: 'text', author_id: 'int' },
55
- permissions: { create: ['@author_id == @user_id'] } // Inlined SQL security
56
- }
57
- };
58
-
59
- export const subscribables = {
60
- post_feed: {
61
- root: { entity: 'posts' },
62
- scopeTables: ['posts']
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
- ### 2. Compile
47
+ **2. Compile:**
68
48
 
69
49
  ```bash
70
- bunx dzql domain.ts -o generated
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
- ## Client Connection & Authentication
102
-
103
- When a client connects to the WebSocket server, it immediately receives a `connection:ready` message containing the authenticated user profile (or `null` if not authenticated).
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
- ### Basic Usage
58
+ **3. Use in your app:**
169
59
 
170
60
  ```typescript
171
- import { useVenueDetailStore } from '@/generated/client/stores';
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
- // loading is false after first data
182
- console.log(loading); // false
183
-
184
- // Subsequent calls with same params return cached subscription
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
- ### Vue Component Patterns
189
-
190
- **Pattern 1: Top-level await (recommended with `<Suspense>`)**
70
+ ## Key Concepts
191
71
 
192
- ```vue
193
- <script setup>
194
- import { useVenueDetailStore } from '@/generated/client/stores';
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
- const props = defineProps(['venueId']);
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
- // Await at top level - component suspends until data arrives
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
- **Pattern 2: Reactive binding with loading state**
216
-
217
- ```vue
218
- <script setup>
219
- import { useVenueDetailStore } from '@/generated/client/stores';
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
- **Pattern 3: Multiple subscriptions**
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
- **User code - just works:**
315
- ```typescript
316
- const venuesStore = useVenuesStore(); // Auto-registers for broadcasts
317
- await venuesStore.search({ org_id: 1 });
318
- // records update automatically when broadcasts arrive - no setup needed!
96
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
97
+ │ Browser │────▶│ Runtime │────▶│ PostgreSQL
98
+ │ Vue + SDK │◀────│ WebSocket │◀────│ + Notify │
99
+ └─────────────┘ └─────────────┘ └─────────────┘
100
+ Pinia Allowlist Compiled
101
+ Stores Gateway SQL
319
102
  ```
320
103
 
321
- ### Broadcast Message Format
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
- ```typescript
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
- ### Entity Notifications
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
- Entities can define `notifications` paths to specify who receives broadcasts:
114
+ ## Package Exports
337
115
 
338
116
  ```typescript
339
- export const entities = {
340
- venues: {
341
- schema: { id: 'serial PRIMARY KEY', org_id: 'int', name: 'text' },
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 |