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