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
package/docs/for_ai.md ADDED
@@ -0,0 +1,653 @@
1
+ # DZQL Guide for AI Assistants
2
+
3
+ This document defines the patterns and conventions for generating valid DZQL domain definitions. Use this guide when asked to "Create a DZQL app" or "Add an entity".
4
+
5
+ ## Quick Start
6
+
7
+ The fastest way to create a new DZQL app:
8
+
9
+ ```bash
10
+ bun create dzql my-app
11
+ cd my-app
12
+ bun install
13
+ bun run db:rebuild
14
+ bun run dev
15
+ ```
16
+
17
+ ## Core Concept: The Domain Definition
18
+
19
+ A DZQL application is defined by a single TypeScript/JavaScript module exporting `entities` and `subscribables`.
20
+
21
+ ### 1. Entity Definition Pattern
22
+
23
+ Each key in `entities` maps to a database table.
24
+
25
+ ```javascript
26
+ export const entities = {
27
+ // Key = Table Name (snake_case recommended)
28
+ [entity_name]: {
29
+
30
+ // 1. Schema: Standard PostgreSQL types
31
+ // Format: 'type constraints'
32
+ schema: {
33
+ id: 'serial PRIMARY KEY',
34
+ name: 'text NOT NULL',
35
+ org_id: 'int REFERENCES organisations(id) ON DELETE CASCADE', // Always define FK constraints
36
+ created_at: 'timestamptz DEFAULT now()'
37
+ },
38
+
39
+ // 2. Configuration
40
+ label: 'name', // Field used for autocomplete/display
41
+ searchable: ['name', 'description'], // Fields indexed for search
42
+
43
+ // 3. Permissions: Row-Level Security DSL
44
+ // Rules are OR-ed together. If any rule passes, access is granted.
45
+ // Empty array [] = Deny All (Default for strictness)
46
+ // ['TRUE'] = Public Access
47
+ permissions: {
48
+ view: ['@org_id->acts_for[org_id=$]{active}.user_id'], // Complex traversal
49
+ create: ['@author_id == @user_id'], // Simple check
50
+ update: ['@id'], // Implies "User ID matches Record ID" (Owner)
51
+ delete: []
52
+ },
53
+
54
+ // 4. Graph Rules: Side Effects & Cascades
55
+ graphRules: {
56
+ // Triggered AFTER successful INSERT
57
+ on_create: {
58
+ action_name: {
59
+ actions: [
60
+ // Database Side Effect
61
+ {
62
+ type: 'create',
63
+ entity: 'notifications',
64
+ data: { user_id: '@user_id', message: 'Welcome' }
65
+ },
66
+ // Async Reactor (External Side Effect via Runtime)
67
+ {
68
+ type: 'reactor',
69
+ name: 'send_email',
70
+ params: { email: '@email' }
71
+ }
72
+ ]
73
+ }
74
+ },
75
+ // Triggered BEFORE DELETE
76
+ on_delete: {
77
+ cascade_cleanup: {
78
+ actions: [
79
+ // Explicit Cascade (if not handled by DB FK)
80
+ { type: 'delete', target: 'comments', params: { post_id: '@id' } }
81
+ ]
82
+ }
83
+ }
84
+ }
85
+ }
86
+ };
87
+ ```
88
+
89
+ ### 2. Permission DSL Guide
90
+
91
+ Permissions are compiled to SQL `EXISTS` clauses.
92
+
93
+ * **Variables:**
94
+ * `@user_id`: The authenticated user's ID.
95
+ * `@id`: The Record's ID (or value of `id` column).
96
+ * `@field`: The value of a field in the record (or input data).
97
+
98
+ * **Patterns:**
99
+ * **Self-Ownership:** `'@user_id == @owner_id'` (or just `'@owner_id'` shorthand).
100
+ * **Traversal:** `'@org_id->acts_for[org_id=$]{active}.user_id'`
101
+ * `@org_id`: Start from this field on the current entity.
102
+ * `->acts_for`: Join to `acts_for` table.
103
+ * `[org_id=$]`: Join condition (`acts_for.org_id = current.org_id`).
104
+ * `{active}`: Filter condition (`acts_for.active = true` or temporal check).
105
+ * `.user_id`: Final check (`acts_for.user_id = @user_id`).
106
+
107
+ ### 3. Subscribable Definition Pattern (The "Smart Store")
108
+
109
+ Subscribables define the *shape* of the data the client needs. They are more than just queries; they define the **Graph Patching Strategy**.
110
+
111
+ ```javascript
112
+ export const subscribables = {
113
+ // Key = Subscription Name
114
+ venue_detail: {
115
+ // 1. Parameters (Inputs)
116
+ params: { venue_id: 'int' },
117
+
118
+ // 2. Root Entity
119
+ root: {
120
+ entity: 'venues',
121
+ key: 'venue_id' // Maps param 'venue_id' to entity PK
122
+ },
123
+
124
+ // 3. Graph Structure (Nested Includes)
125
+ includes: {
126
+ // Simple relation
127
+ org: { relation: 'org', entity: 'organisations' },
128
+
129
+ // Nested relation
130
+ sites: {
131
+ relation: 'sites',
132
+ entity: 'sites',
133
+ // Recursive nesting
134
+ includes: {
135
+ allocations: { relation: 'allocations', entity: 'allocations' }
136
+ }
137
+ }
138
+ },
139
+
140
+ // 4. Scope Tables (Crucial for Realtime)
141
+ // List ALL tables that appear in this graph.
142
+ // The Runtime uses this to route events.
143
+ scopeTables: ['venues', 'organisations', 'sites', 'allocations'],
144
+
145
+ // 5. Subscription Permission
146
+ // Who is allowed to listen to this feed?
147
+ canSubscribe: ['@venue_id->venues.org_id->acts_for[org_id=$]{active}.user_id']
148
+ }
149
+ };
150
+ ```
151
+
152
+ ### When to use what?
153
+
154
+ * **Entities:** Always define entities for every physical table.
155
+ * **Subscribables:** Define a subscribable for every **Screen** or **Major Component** in the UI. (e.g., `dashboard`, `profile`, `item_detail`).
156
+ * *Do not* write manual API fetchers. Use subscribables to generate "Smart Stores" that self-update.
157
+
158
+ ### Using Generated Subscribable Stores
159
+
160
+ Each subscribable in your domain generates a Pinia store with this structure:
161
+
162
+ ```typescript
163
+ // Generated store API
164
+ const store = useVenueDetailStore();
165
+
166
+ store.bind(params) // Async - subscribe and get { data, loading, ready }
167
+ store.unbind(params) // Unsubscribe and remove document from store
168
+ store.documents // Ref - all bound documents keyed by JSON params
169
+ ```
170
+
171
+ **Basic Usage:**
172
+
173
+ ```typescript
174
+ import { useVenueDetailStore } from '@/generated/client/stores';
175
+
176
+ const store = useVenueDetailStore();
177
+
178
+ // bind() returns a Promise that resolves when first data arrives
179
+ const { data, loading, ready } = await store.bind({ venue_id: 1 });
180
+
181
+ // data is now populated - no need to check loading
182
+ console.log(data.name); // 'My Venue'
183
+ console.log(data.org.name); // 'My Org' (nested include)
184
+ console.log(data.sites); // [{...}, {...}] (array of related records)
185
+
186
+ // Subsequent calls with same params return cached subscription immediately
187
+ const cached = await store.bind({ venue_id: 1 }); // No new subscription created
188
+ ```
189
+
190
+ **Vue Component Pattern (with Suspense):**
191
+
192
+ ```vue
193
+ <script setup>
194
+ import { useVenueDetailStore } from '@/generated/client/stores';
195
+
196
+ const props = defineProps(['venueId']);
197
+ const store = useVenueDetailStore();
198
+
199
+ // Top-level await - component suspends until data arrives
200
+ const { data } = await store.bind({ venue_id: props.venueId });
201
+ </script>
202
+
203
+ <template>
204
+ <h1>{{ data.name }}</h1>
205
+ <p>Org: {{ data.org.name }}</p>
206
+ <ul>
207
+ <li v-for="site in data.sites" :key="site.id">
208
+ {{ site.name }}
209
+ </li>
210
+ </ul>
211
+ </template>
212
+ ```
213
+
214
+ **Vue Component Pattern (with loading state):**
215
+
216
+ ```vue
217
+ <script setup>
218
+ import { useVenueDetailStore } from '@/generated/client/stores';
219
+ import { ref, onMounted, watch } from 'vue';
220
+
221
+ const props = defineProps(['venueId']);
222
+ const store = useVenueDetailStore();
223
+ const doc = ref({ data: null, loading: true });
224
+
225
+ onMounted(async () => {
226
+ doc.value = await store.bind({ venue_id: props.venueId });
227
+ });
228
+
229
+ // Re-bind when venueId changes
230
+ watch(() => props.venueId, async (newId) => {
231
+ doc.value = await store.bind({ venue_id: newId });
232
+ });
233
+ </script>
234
+
235
+ <template>
236
+ <div v-if="doc.loading">Loading...</div>
237
+ <template v-else>
238
+ <h1>{{ doc.data.name }}</h1>
239
+ </template>
240
+ </template>
241
+ ```
242
+
243
+ **Accessing Multiple Subscriptions:**
244
+
245
+ ```typescript
246
+ const store = useVenueDetailStore();
247
+
248
+ // Each unique params set creates a separate subscription
249
+ await store.bind({ venue_id: 1 });
250
+ await store.bind({ venue_id: 2 });
251
+
252
+ // Access all documents via store.documents
253
+ for (const [key, docState] of Object.entries(store.documents)) {
254
+ console.log(key, docState.data);
255
+ }
256
+ // '{"venue_id":1}' { id: 1, name: 'Venue 1', ... }
257
+ // '{"venue_id":2}' { id: 2, name: 'Venue 2', ... }
258
+ ```
259
+
260
+ **Key Points:**
261
+ - `bind()` is async and awaits first data before returning
262
+ - Same params = same cached subscription (deduplication by JSON key)
263
+ - The `ready` Promise is stored for repeat callers to await
264
+ - **Stores own their data** - the WebSocket is just transport
265
+ - Realtime patches are applied by the store's `applyPatch()` function
266
+ - Data is reactive - changes trigger Vue reactivity automatically
267
+ - The store routes patch events by table name to the correct location in the document graph
268
+
269
+ ### Common Patterns
270
+
271
+ **1. The "Owner" Pattern:**
272
+ ```javascript
273
+ create: ['@author_id == @user_id']
274
+ ```
275
+
276
+ **2. The "Organization Member" Pattern:**
277
+ ```javascript
278
+ view: ['@org_id->memberships[org_id=$].user_id']
279
+ ```
280
+
281
+ **3. The "Creator Side Effect" Pattern:**
282
+ Use `graphRules.on_create` to create related records automatically (e.g., creating a default 'Settings' record when a 'User' is created).
283
+
284
+ **4. The "Reactor" Pattern:**
285
+ Use `type: 'reactor'` for anything that requires Node.js (Email, Stripe, AI processing). Do not try to do complex logic in SQL.
286
+
287
+ ---
288
+
289
+ ## Custom Functions
290
+
291
+ TZQL supports two types of custom functions that can be called via RPC:
292
+
293
+ ### 1. SQL Custom Functions
294
+
295
+ SQL custom functions are defined in your domain and compiled into the database migrations. They run inside PostgreSQL and are ideal for complex queries, aggregations, or operations that benefit from database-level performance.
296
+
297
+ **Domain Definition:**
298
+
299
+ ```javascript
300
+ // domain.js
301
+ export const entities = { /* ... */ };
302
+ export const subscribables = { /* ... */ };
303
+
304
+ // Add custom SQL functions
305
+ export const customFunctions = [
306
+ {
307
+ name: 'calculate_org_stats',
308
+ sql: `
309
+ CREATE OR REPLACE FUNCTION dzql_v2.calculate_org_stats(p_user_id int, p_params jsonb)
310
+ RETURNS jsonb LANGUAGE plpgsql AS $$
311
+ DECLARE
312
+ v_org_id int;
313
+ v_venue_count int;
314
+ v_total_revenue numeric;
315
+ BEGIN
316
+ v_org_id := (p_params->>'org_id')::int;
317
+
318
+ -- Permission check (optional but recommended)
319
+ IF NOT EXISTS (
320
+ SELECT 1 FROM acts_for
321
+ WHERE user_id = p_user_id AND org_id = v_org_id AND active = true
322
+ ) THEN
323
+ RAISE EXCEPTION 'permission_denied' USING ERRCODE = 'P0001';
324
+ END IF;
325
+
326
+ SELECT COUNT(*) INTO v_venue_count
327
+ FROM venues WHERE org_id = v_org_id;
328
+
329
+ SELECT COALESCE(SUM(amount), 0) INTO v_total_revenue
330
+ FROM orders WHERE org_id = v_org_id;
331
+
332
+ RETURN jsonb_build_object(
333
+ 'org_id', v_org_id,
334
+ 'venue_count', v_venue_count,
335
+ 'total_revenue', v_total_revenue
336
+ );
337
+ END;
338
+ $$;
339
+ `,
340
+ args: ['p_user_id', 'p_params'] // Optional, defaults to these
341
+ }
342
+ ];
343
+ ```
344
+
345
+ **Calling from Client:**
346
+
347
+ ```typescript
348
+ // The function is automatically added to the client SDK
349
+ const stats = await ws.api.calculate_org_stats({ org_id: 1 });
350
+ console.log(stats.venue_count, stats.total_revenue);
351
+ ```
352
+
353
+ **Key Points:**
354
+ - Functions must be in the `dzql_v2` schema
355
+ - Standard signature: `(p_user_id int, p_params jsonb) RETURNS jsonb`
356
+ - Automatically added to the manifest allowlist (security)
357
+ - Compiled into the database migrations
358
+
359
+ ### 2. JavaScript Custom Functions
360
+
361
+ JavaScript custom functions run in the Bun/Node runtime. They are ideal for:
362
+ - External API calls (Stripe, SendGrid, etc.)
363
+ - Complex business logic that's easier in JS than SQL
364
+ - Operations that need access to environment variables or external services
365
+
366
+ **Registration (in your server startup):**
367
+
368
+ ```typescript
369
+ // server.ts or wherever you start your runtime
370
+ import { registerJsFunction } from 'tzql/runtime';
371
+
372
+ // Simple function
373
+ registerJsFunction('hello_world', async (ctx) => {
374
+ return {
375
+ message: `Hello, User ${ctx.userId}!`,
376
+ timestamp: new Date().toISOString()
377
+ };
378
+ });
379
+
380
+ // Function with database access
381
+ registerJsFunction('get_user_dashboard', async (ctx) => {
382
+ const { userId, params, db } = ctx;
383
+
384
+ // Query the database
385
+ const orgs = await db.query(
386
+ 'SELECT o.* FROM organisations o JOIN acts_for af ON o.id = af.org_id WHERE af.user_id = $1 AND af.active = true',
387
+ [userId]
388
+ );
389
+
390
+ const venues = await db.query(
391
+ 'SELECT * FROM venues WHERE org_id = ANY($1)',
392
+ [orgs.map(o => o.id)]
393
+ );
394
+
395
+ return {
396
+ organizations: orgs,
397
+ venues: venues,
398
+ total_venues: venues.length
399
+ };
400
+ });
401
+
402
+ // Function calling external API
403
+ registerJsFunction('send_notification', async (ctx) => {
404
+ const { userId, params } = ctx;
405
+
406
+ // Call external service
407
+ const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
408
+ method: 'POST',
409
+ headers: {
410
+ 'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,
411
+ 'Content-Type': 'application/json'
412
+ },
413
+ body: JSON.stringify({
414
+ to: params.email,
415
+ subject: params.subject,
416
+ content: params.message
417
+ })
418
+ });
419
+
420
+ return { success: response.ok };
421
+ });
422
+ ```
423
+
424
+ **Calling from Client:**
425
+
426
+ ```typescript
427
+ // JS functions are called the same way as SQL functions
428
+ const dashboard = await ws.api.get_user_dashboard({});
429
+ const result = await ws.api.send_notification({
430
+ email: 'user@example.com',
431
+ subject: 'Hello',
432
+ message: 'Welcome!'
433
+ });
434
+ ```
435
+
436
+ **Context Object:**
437
+
438
+ ```typescript
439
+ interface JsFunctionContext {
440
+ userId: number; // Authenticated user's ID
441
+ params: any; // Parameters passed from client
442
+ db: {
443
+ query(sql: string, params?: any[]): Promise<any[]>; // Database access
444
+ };
445
+ }
446
+ ```
447
+
448
+ **Key Points:**
449
+ - JS functions take precedence over SQL functions with the same name
450
+ - No manifest entry needed - registration is enough
451
+ - Full access to Node.js/Bun APIs (fetch, fs, etc.)
452
+ - Can query the database via `ctx.db.query()`
453
+ - Errors thrown are propagated to the client
454
+
455
+ ### When to Use Which?
456
+
457
+ | Use Case | SQL | JavaScript |
458
+ |----------|-----|------------|
459
+ | Complex aggregations | ✅ | |
460
+ | Data transformations | ✅ | |
461
+ | External API calls | | ✅ |
462
+ | Email/SMS notifications | | ✅ |
463
+ | File processing | | ✅ |
464
+ | Payment processing | | ✅ |
465
+ | Performance-critical queries | ✅ | |
466
+ | Access to env variables | | ✅ |
467
+ | Multi-step transactions | ✅ | |
468
+ | Real-time calculations | ✅ | |
469
+
470
+ ---
471
+
472
+ ## Unmanaged Entities (Junction Tables)
473
+
474
+ For junction tables used in many-to-many relationships, you typically don't want TZQL to generate CRUD functions. These tables are managed via the M2M relationship on the parent entity.
475
+
476
+ Use `managed: false` to skip CRUD generation:
477
+
478
+ ```javascript
479
+ export const entities = {
480
+ brands: {
481
+ schema: {
482
+ id: 'serial PRIMARY KEY',
483
+ name: 'text NOT NULL'
484
+ },
485
+ // M2M relationship manages brand_tags automatically
486
+ manyToMany: {
487
+ tags: {
488
+ junctionTable: 'brand_tags',
489
+ localKey: 'brand_id',
490
+ foreignKey: 'tag_id',
491
+ targetEntity: 'tags',
492
+ idField: 'tag_ids'
493
+ }
494
+ }
495
+ },
496
+
497
+ tags: {
498
+ schema: {
499
+ id: 'serial PRIMARY KEY',
500
+ name: 'text NOT NULL'
501
+ }
502
+ },
503
+
504
+ // Junction table - no CRUD functions generated
505
+ brand_tags: {
506
+ schema: {
507
+ brand_id: 'int NOT NULL REFERENCES brands(id) ON DELETE CASCADE',
508
+ tag_id: 'int NOT NULL REFERENCES tags(id) ON DELETE CASCADE'
509
+ },
510
+ primaryKey: ['brand_id', 'tag_id'],
511
+ managed: false // Skip CRUD generation
512
+ }
513
+ };
514
+ ```
515
+
516
+ **What `managed: false` does:**
517
+ - The table schema is still created in the database
518
+ - No `get_brand_tags`, `save_brand_tags`, `delete_brand_tags`, etc. functions are generated
519
+ - No manifest entries for CRUD operations
520
+ - The junction table is managed via the parent entity's M2M operations
521
+
522
+ ---
523
+
524
+ ## Client Connection & Authentication
525
+
526
+ 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).
527
+
528
+ ### Connection Flow
529
+
530
+ 1. Client connects with optional `?token=...` in URL
531
+ 2. Server validates token (if present) and fetches user profile
532
+ 3. Server sends: `{"method": "connection:ready", "params": {"user": {...} | null}}`
533
+ 4. Client knows auth state immediately, can render accordingly
534
+
535
+ ### WebSocketManager API
536
+
537
+ ```typescript
538
+ import { WebSocketManager } from 'dzql/client';
539
+
540
+ const ws = new WebSocketManager();
541
+ await ws.connect('ws://localhost:3000/ws');
542
+
543
+ // Connection state properties
544
+ ws.ready // boolean - true after connection:ready received
545
+ ws.user // user profile object or null if anonymous
546
+
547
+ // Register callback for ready state (called immediately if already ready)
548
+ const unsubscribe = ws.onReady((user) => {
549
+ if (user) {
550
+ console.log('Authenticated as:', user.email);
551
+ } else {
552
+ console.log('Anonymous connection');
553
+ }
554
+ });
555
+
556
+ // Authentication methods
557
+ await ws.login({ email: '...', password: '...' }); // Stores token in localStorage
558
+ await ws.register({ email: '...', password: '...' }); // Stores token in localStorage
559
+ await ws.logout(); // Clears token, user state, and reconnects
560
+ ```
561
+
562
+ ### Vue/Pinia Usage Pattern
563
+
564
+ ```vue
565
+ <template>
566
+ <div v-if="!ws.ready">Loading...</div>
567
+ <LoginModal v-else-if="!ws.user" />
568
+ <RouterView v-else />
569
+ </template>
570
+ ```
571
+
572
+ ### Why This Matters
573
+
574
+ - **Single source of truth:** WebSocket connection determines auth state, not localStorage
575
+ - **No race conditions:** UI waits for `connection:ready` before rendering
576
+ - **Simpler client code:** No need for separate auth check after connect
577
+ - **Better UX:** App shows loading state until connection ready, then immediately correct view
578
+
579
+ ---
580
+
581
+ ## CLI Integration with Namespace
582
+
583
+ TZQL provides a namespace export for CLI tools like `invokej` to interact with the database directly without going through the WebSocket runtime.
584
+
585
+ ### Setup
586
+
587
+ ```typescript
588
+ // cli.ts or server-side script
589
+ import { DzqlNamespace } from 'dzql/namespace';
590
+ import postgres from 'postgres';
591
+
592
+ const sql = postgres(process.env.DATABASE_URL);
593
+ const manifest = await import('./dist/runtime/manifest.json');
594
+
595
+ const dzql = new DzqlNamespace(sql, manifest);
596
+ ```
597
+
598
+ ### CRUD Operations
599
+
600
+ ```typescript
601
+ // Get a record
602
+ const venue = await tzql.get('venues', { id: 1 }, userId);
603
+
604
+ // Save (create or update)
605
+ const newVenue = await tzql.save('venues', { name: 'New Venue', org_id: 1 }, userId);
606
+
607
+ // Delete
608
+ const deleted = await tzql.delete('venues', { id: 1 }, userId);
609
+
610
+ // Search with filters
611
+ const venues = await tzql.search('venues', { org_id: 1, limit: 10 }, userId);
612
+
613
+ // Lookup for autocomplete
614
+ const options = await tzql.lookup('venues', { q: 'test' }, userId);
615
+ ```
616
+
617
+ ### Ad-hoc Function Calls
618
+
619
+ Call any function in the manifest directly:
620
+
621
+ ```typescript
622
+ // Call a custom function
623
+ const result = await tzql.call('calculate_org_stats', { org_id: 1 }, userId);
624
+
625
+ // Call a subscribable getter
626
+ const detail = await tzql.call('get_venue_detail', { venue_id: 1 }, userId);
627
+ ```
628
+
629
+ ### List Available Functions
630
+
631
+ ```typescript
632
+ // Get all functions from manifest
633
+ const functions = tzql.functions();
634
+ // Returns: ['login_user', 'register_user', 'get_venues', 'save_venues', ...]
635
+ ```
636
+
637
+ ### Use with invokej
638
+
639
+ The namespace is designed for use with `invokej`, a CLI tool for invoking functions:
640
+
641
+ ```bash
642
+ # In your invokej configuration, register the TZQL namespace
643
+ invokej tzql:get venues '{"id": 1}'
644
+ invokej tzql:save venues '{"name": "Updated Venue", "id": 1}'
645
+ invokej tzql:call calculate_org_stats '{"org_id": 1}'
646
+ invokej tzql:functions
647
+ ```
648
+
649
+ **Key Points:**
650
+ - All operations respect the same permissions as the WebSocket runtime
651
+ - The `userId` parameter is required for permission checks
652
+ - Operations are atomic (single transaction)
653
+ - Results are returned as JSON objects