dzql 0.5.32 → 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/README.md CHANGED
@@ -1,54 +1,311 @@
1
- # DZQL Documentation
1
+ # TZQL: The Compile-Only Realtime Database Framework
2
2
 
3
- Complete documentation for the DZQL PostgreSQL-powered framework.
3
+ TZQL ("The 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.
4
4
 
5
- ## 📚 Getting Started
5
+ ## The Problem
6
6
 
7
- New to DZQL? Start here:
7
+ Building realtime apps is hard. You typically have to:
8
+ 1. **Sync State:** Manually keep your frontend Pinia/Redux store in sync with your backend database.
9
+ 2. **Manage Permissions:** Re-implement row-level security in your API layer (and hope it matches your DB).
10
+ 3. **Handle Atomicity:** Ensure that complex operations (e.g., "Create Order + Reserve Inventory + Notify User") happen in a single transaction.
11
+ 4. **Optimistic Updates:** Write complex client-side logic to "guess" the server's response, often leading to data divergence.
8
12
 
9
- - **[Tutorial](getting-started/tutorial.md)** - Complete step-by-step guide with a working todo app
10
- - **[Interpreter vs Compiler](guides/interpreter-vs-compiler.md)** - Understand the two execution modes
11
- - **[Subscriptions Quick Start](getting-started/subscriptions-quick-start.md)** - Get real-time subscriptions working in 5 minutes
13
+ ## The TZQL Solution
12
14
 
13
- ## 📖 Guides
15
+ TZQL takes a radically different approach: **Compilation**.
14
16
 
15
- Feature-specific guides and how-tos:
17
+ Instead of a heavy runtime framework, you define your **Domain Schema** (Entities, Relationships, Permissions) in a simple TypeScript configuration. TZQL compiles this definition into:
16
18
 
17
- - **[Live Query Subscriptions](guides/subscriptions.md)** - Real-time denormalized documents
18
- - **[Many-to-Many Relationships](guides/many-to-many.md)** - Junction table management
19
- - **[Composite Primary Keys](guides/composite-primary-keys.md)** - Tables with compound keys
20
- - **[Field Defaults](guides/field-defaults.md)** - Auto-populate fields on create
21
- - **[Custom Functions](guides/custom-functions.md)** - Extend with PostgreSQL or Bun functions
22
- - **[Client Stores](guides/client-stores.md)** - Pinia store patterns for Vue.js
19
+ 1. **Optimized SQL:** Specialized PostgreSQL functions (`save_order`, `get_product`) with *inlined* permission checks and *atomic* graph operations.
20
+ 2. **Type-Safe Client SDK:** A generated TypeScript client that knows your exact API surface.
21
+ 3. **Smart Pinia Stores:** Generated Vue stores that automatically handle realtime synchronization using atomic "Patch Events" from the database.
23
22
 
24
- ## 📘 Reference
23
+ ### Key Features
25
24
 
26
- Complete API documentation:
25
+ * **Zero Runtime Interpretation:** No slow ORM query builders. Everything is compiled to native PL/pgSQL.
26
+ * **Security by Construction:** The runtime is a "dumb" gateway that routes requests by OID allowlist. It *cannot* execute arbitrary SQL.
27
+ * **Atomic Everything:** Complex graph operations (cascading creates/deletes) happen in a single database transaction.
28
+ * **Realtime by Default:** Every database write emits an atomic event batch. The client SDK automatically patches your local state. No "refetching" required.
29
+ * **JavaScript/TypeScript Native:** Define your schema in code you understand, get full type safety end-to-end.
27
30
 
28
- - **[API Reference](reference/api.md)** - The 5 operations, entities, permissions, graph rules
29
- - **[Client API](reference/client.md)** - WebSocket client and connection management
30
- - **[Compiler](compiler/)** - Entity compilation, code generation, and coding standards
31
+ ## Quick Start
31
32
 
32
- ### Compiler Documentation
33
+ ### 1. Define your Domain (`domain.ts`)
33
34
 
34
- - [Quickstart](compiler/QUICKSTART.md) - Get started with the DZQL compiler
35
- - [Advanced Filters](compiler/ADVANCED_FILTERS.md) - Complex search operators
36
- - [Coding Standards](compiler/CODING_STANDARDS.md) - Best practices for DZQL code
37
- - [Comparison](compiler/COMPARISON.md) - Runtime vs compiled side-by-side
35
+ ```typescript
36
+ export const entities = {
37
+ posts: {
38
+ schema: { id: 'serial PRIMARY KEY', title: 'text', author_id: 'int' },
39
+ permissions: { create: ['@author_id == @user_id'] } // Inlined SQL security
40
+ }
41
+ };
38
42
 
39
- ## 🤖 For AI Assistants
43
+ export const subscribables = {
44
+ post_feed: {
45
+ root: { entity: 'posts' },
46
+ scopeTables: ['posts']
47
+ }
48
+ };
49
+ ```
40
50
 
41
- - **[Claude Guide](for-ai/claude-guide.md)** - Complete guide for AI-assisted DZQL development
51
+ ### 2. Compile
42
52
 
43
- ## 🔗 Quick Links
53
+ ```bash
54
+ bun run tzql compile domain.ts -o src/generated
55
+ ```
44
56
 
45
- - [npm Package](https://www.npmjs.com/package/dzql)
46
- - [GitHub Repository](https://github.com/blueshed/dzql)
47
- - [Issue Tracker](https://github.com/blueshed/dzql/issues)
57
+ ### 3. Use in Client
48
58
 
49
- ## Need Help?
59
+ ```typescript
60
+ import { usePostFeedStore } from '@/generated/client/stores';
50
61
 
51
- - 📖 Check the guides above
52
- - 🐛 [Report an issue](https://github.com/blueshed/dzql/issues)
53
- - 💬 [Start a discussion](https://github.com/blueshed/dzql/discussions)
54
- - 🤖 Ask your AI assistant (they have access to this documentation!)
62
+ const feed = usePostFeedStore();
63
+ // Automatically fetches data AND subscribes to realtime updates
64
+ // bind() is async - awaits until first data arrives
65
+ const { data, loading } = await feed.bind({ user_id: 1 });
66
+ // data.value is now populated
67
+ ```
68
+
69
+ ## Architecture
70
+
71
+ * **Compiler:** CLI tool that analyzes your domain and generates artifacts.
72
+ * **Runtime:** A lightweight Bun/Node server that handles Auth (JWT) and WebSocket connection pooling.
73
+ * **Client:** A robust WebSocket SDK that manages reconnection and dispatches atomic patches to stores.
74
+ * **Namespace:** Direct database access for CLI tools like `invokej`.
75
+
76
+ ## Package Exports
77
+
78
+ ```typescript
79
+ import { ... } from 'tzql'; // Runtime server
80
+ import { ... } from 'tzql/client'; // WebSocket client SDK
81
+ import { ... } from 'tzql/compiler'; // CLI compiler
82
+ import { TzqlNamespace } from 'tzql/namespace'; // CLI/invokej integration
83
+ ```
84
+
85
+ ## Client Connection & Authentication
86
+
87
+ 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).
88
+
89
+ ### Connection Flow
90
+
91
+ 1. Client connects with optional `?token=...` in URL
92
+ 2. Server validates token (if present) and fetches user profile
93
+ 3. Server sends: `{"method": "connection:ready", "params": {"user": {...} | null}}`
94
+ 4. Client knows auth state immediately
95
+
96
+ ### Client API
97
+
98
+ ```typescript
99
+ import { WebSocketManager } from 'tzql/client';
100
+
101
+ const ws = new WebSocketManager();
102
+ await ws.connect('ws://localhost:3000/ws');
103
+
104
+ // Check connection state
105
+ console.log(ws.ready); // true after connection:ready received
106
+ console.log(ws.user); // user profile object or null
107
+
108
+ // Register callback for ready state
109
+ ws.onReady((user) => {
110
+ if (user) {
111
+ console.log('Authenticated as:', user.email);
112
+ } else {
113
+ console.log('Anonymous connection');
114
+ }
115
+ });
116
+
117
+ // Authentication methods
118
+ await ws.login({ email: '...', password: '...' }); // Stores token in localStorage
119
+ await ws.logout(); // Clears token and user state
120
+ ```
121
+
122
+ ### Vue/Pinia Usage Pattern
123
+
124
+ ```vue
125
+ <template>
126
+ <div v-if="!ws.ready">Loading...</div>
127
+ <LoginModal v-else-if="!ws.user" />
128
+ <RouterView v-else />
129
+ </template>
130
+ ```
131
+
132
+ ## Generated Pinia Subscribable Stores
133
+
134
+ TZQL generates Pinia stores for each subscribable that handle:
135
+ - Initial data fetch via WebSocket subscription
136
+ - Automatic realtime patching when related data changes
137
+ - Deduplication of subscriptions by parameter key
138
+
139
+ ### Store Structure
140
+
141
+ Each generated store exports:
142
+
143
+ ```typescript
144
+ const store = useVenueDetailStore();
145
+
146
+ // Main API
147
+ store.bind(params) // Async - subscribes and returns { data, loading, ready }
148
+ store.unbind(params) // Unsubscribes and removes document from store
149
+ store.documents // Ref containing all bound documents keyed by JSON.stringify(params)
150
+ ```
151
+
152
+ ### Basic Usage
153
+
154
+ ```typescript
155
+ import { useVenueDetailStore } from '@/generated/client/stores';
156
+
157
+ const store = useVenueDetailStore();
158
+
159
+ // bind() is async - returns when first data arrives
160
+ const { data, loading, ready } = await store.bind({ venue_id: 1 });
161
+
162
+ // data is reactive and contains the document
163
+ console.log(data); // { id: 1, name: 'My Venue', sites: [...], org: {...} }
164
+
165
+ // loading is false after first data
166
+ console.log(loading); // false
167
+
168
+ // Subsequent calls with same params return cached subscription
169
+ const same = await store.bind({ venue_id: 1 }); // Returns immediately, no new subscription
170
+ ```
171
+
172
+ ### Vue Component Patterns
173
+
174
+ **Pattern 1: Top-level await (recommended with `<Suspense>`)**
175
+
176
+ ```vue
177
+ <script setup>
178
+ import { useVenueDetailStore } from '@/generated/client/stores';
179
+
180
+ const props = defineProps(['venueId']);
181
+ const store = useVenueDetailStore();
182
+
183
+ // Await at top level - component suspends until data arrives
184
+ const { data } = await store.bind({ venue_id: props.venueId });
185
+ </script>
186
+
187
+ <template>
188
+ <!-- data is guaranteed to be populated -->
189
+ <h1>{{ data.name }}</h1>
190
+ <p>Organization: {{ data.org.name }}</p>
191
+ <ul>
192
+ <li v-for="site in data.sites" :key="site.id">
193
+ {{ site.name }} ({{ site.allocations.length }} allocations)
194
+ </li>
195
+ </ul>
196
+ </template>
197
+ ```
198
+
199
+ **Pattern 2: Reactive binding with loading state**
200
+
201
+ ```vue
202
+ <script setup>
203
+ import { useVenueDetailStore } from '@/generated/client/stores';
204
+ import { ref, onMounted, watch } from 'vue';
205
+
206
+ const props = defineProps(['venueId']);
207
+ const store = useVenueDetailStore();
208
+ const docState = ref({ data: null, loading: true });
209
+
210
+ onMounted(async () => {
211
+ docState.value = await store.bind({ venue_id: props.venueId });
212
+ });
213
+
214
+ // Re-bind when venueId changes
215
+ watch(() => props.venueId, async (newId) => {
216
+ docState.value = await store.bind({ venue_id: newId });
217
+ });
218
+ </script>
219
+
220
+ <template>
221
+ <div v-if="docState.loading">Loading...</div>
222
+ <div v-else>
223
+ <h1>{{ docState.data.name }}</h1>
224
+ </div>
225
+ </template>
226
+ ```
227
+
228
+ **Pattern 3: Multiple subscriptions**
229
+
230
+ ```vue
231
+ <script setup>
232
+ import { useVenueDetailStore } from '@/generated/client/stores';
233
+
234
+ const store = useVenueDetailStore();
235
+
236
+ // Bind multiple venues - each gets its own cached subscription
237
+ const venues = await Promise.all([
238
+ store.bind({ venue_id: 1 }),
239
+ store.bind({ venue_id: 2 }),
240
+ store.bind({ venue_id: 3 }),
241
+ ]);
242
+ </script>
243
+ ```
244
+
245
+ ### Accessing All Documents
246
+
247
+ The store's `documents` ref contains all bound subscriptions:
248
+
249
+ ```typescript
250
+ const store = useVenueDetailStore();
251
+
252
+ await store.bind({ venue_id: 1 });
253
+ await store.bind({ venue_id: 2 });
254
+
255
+ // Access all documents
256
+ console.log(store.documents);
257
+ // {
258
+ // '{"venue_id":1}': { data: {...}, loading: false, ready: Promise },
259
+ // '{"venue_id":2}': { data: {...}, loading: false, ready: Promise }
260
+ // }
261
+ ```
262
+
263
+ ### How Realtime Works
264
+
265
+ The WebSocket is a pure transport layer - it does not manage or cache data. Stores own their data.
266
+
267
+ When data changes in the database:
268
+ 1. PostgreSQL triggers emit atomic events to the `dzql_v2.events` table
269
+ 2. The runtime calls `*_affected_keys()` to find which subscriptions are affected
270
+ 3. Events are broadcast to subscribed clients via WebSocket
271
+ 4. The **store's** `subscription_event` function receives the event
272
+ 5. The store's `applyPatch` function updates its local document in-place
273
+ 6. Vue reactivity updates the UI automatically
274
+
275
+ **No refetching required** - changes are applied incrementally via patching.
276
+
277
+ ### Patch Events
278
+
279
+ The store receives events with this structure:
280
+
281
+ ```typescript
282
+ {
283
+ table: 'sites', // Which table changed
284
+ op: 'insert' | 'update' | 'delete',
285
+ pk: { id: 123 }, // Primary key of affected row
286
+ data: { ... } // Full row data
287
+ }
288
+ ```
289
+
290
+ The generated `applyPatch` function routes events to the correct location in the document graph based on the subscribable's `includes` structure.
291
+
292
+ ## Why "Compile-Only"?
293
+
294
+ By moving complexity to build-time, we get:
295
+ * **Performance:** The database does the heavy lifting.
296
+ * **Correctness:** If it compiles, your permissions and relationships are valid.
297
+ * **Simplicity:** The runtime is tiny and easy to audit.
298
+
299
+ ## Entity Options
300
+
301
+ | Option | Type | Description |
302
+ |--------|------|-------------|
303
+ | `schema` | object | Column definitions with PostgreSQL types |
304
+ | `primaryKey` | string[] | Composite primary key fields (default: `['id']`) |
305
+ | `label` | string | Field used for display/autocomplete |
306
+ | `softDelete` | boolean | Use `deleted_at` instead of hard delete |
307
+ | `managed` | boolean | Set to `false` to skip CRUD generation (for junction tables) |
308
+ | `permissions` | object | Row-level security rules |
309
+ | `graphRules` | object | Side effects on create/update/delete |
310
+ | `manyToMany` | object | M2M relationship definitions |
311
+ | `fieldDefaults` | object | Default values for fields on INSERT |
@@ -0,0 +1,85 @@
1
+ # tzql Bug Report
2
+
3
+ ## Generated Store applyPatch Doesn't Match Data Structure
4
+
5
+ **Severity:** High (Breaking)
6
+
7
+ **Status:** ✅ FIXED
8
+
9
+ **Description:**
10
+ The generated Pinia store's `applyPatch` function assumes a flat data structure, but the subscribable SQL was returning nested wrapper objects.
11
+
12
+ **SQL was returning:**
13
+ ```json
14
+ {
15
+ "venues": {...},
16
+ "sites": [
17
+ { "sites": { "id": 1, "name": "..." }, "allocations": [...] },
18
+ { "sites": { "id": 2, "name": "..." }, "allocations": [...] }
19
+ ]
20
+ }
21
+ ```
22
+
23
+ **Fix:**
24
+ Updated `subscribable_sql.ts` to use JSONB concatenation (`||`) to merge entity fields with nested includes into a flat structure:
25
+
26
+ ```sql
27
+ SELECT jsonb_agg(
28
+ row_to_json(rel.*) || jsonb_build_object(
29
+ 'allocations', COALESCE((
30
+ SELECT jsonb_agg(row_to_json(nested.*))
31
+ FROM allocations nested
32
+ WHERE nested.site_id = rel.id
33
+ ), '[]'::jsonb))
34
+ )
35
+ ```
36
+
37
+ **SQL now returns:**
38
+ ```json
39
+ {
40
+ "venues": {...},
41
+ "sites": [
42
+ { "id": 1, "name": "...", "allocations": [...] },
43
+ { "id": 2, "name": "...", "allocations": [...] }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ This flat structure matches what `applyPatch` and `handleArrayPatch` expect, so realtime updates now work correctly for nested includes.
49
+
50
+ ---
51
+
52
+ ## Bug 2: json/jsonb Type Mismatch in Subscribable SQL
53
+
54
+ **Severity:** Critical (Blocking)
55
+
56
+ **Status:** ✅ FIXED
57
+
58
+ **Description:**
59
+ The generated subscribable SQL used `row_to_json()` which returns `json` type, but concatenated it with `jsonb_build_object()` which returns `jsonb`. PostgreSQL cannot concatenate these types.
60
+
61
+ **Generated SQL (before):**
62
+ ```sql
63
+ row_to_json(rel.*) || jsonb_build_object(...)
64
+ ```
65
+
66
+ **Error:**
67
+ ```
68
+ operator does not exist: json || jsonb
69
+ ```
70
+
71
+ **Fix:**
72
+ Changed to use `to_jsonb()` instead of `row_to_json()` when concatenating with nested includes:
73
+
74
+ ```sql
75
+ to_jsonb(rel.*) || jsonb_build_object(...)
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Environment
81
+
82
+ - tzql version: local development (linked)
83
+ - Database: PostgreSQL 17 (Docker)
84
+ - Client: Vue 3 + Pinia + TypeScript
85
+ - Runtime: Bun
@@ -0,0 +1,57 @@
1
+ # Feature Request: Send User Profile on WebSocket Connect
2
+
3
+ **Status: IMPLEMENTED**
4
+
5
+ ## Summary
6
+
7
+ When a client connects to the tzql WebSocket server, the server should immediately send a connection:ready message containing the authenticated user profile (or null if not authenticated).
8
+
9
+ ## Current Behavior
10
+
11
+ 1. Client connects with optional ?token=... in URL
12
+ 2. Server opens connection but sends nothing
13
+ 3. Client must call auth RPC to authenticate, then separately fetch profile
14
+ 4. Client has no immediate knowledge of auth state
15
+
16
+ ## Proposed Behavior
17
+
18
+ 1. Client connects with optional ?token=... in URL
19
+ 2. Server validates token (if present) and fetches user profile
20
+ 3. Server sends first message:
21
+
22
+ {"method": "connection:ready", "params": {"user": {"id": 1, "name": "...", "email": "..."} | null}}
23
+
24
+ 4. Client knows auth state immediately, can render accordingly
25
+
26
+ ## Why This Matters
27
+
28
+ - Single source of truth: WebSocket connection determines auth state, not localStorage
29
+ - No race conditions: UI waits for connection:ready before rendering
30
+ - Simpler client code: No need for separate auth check after connect
31
+ - Better UX: App shows loading state until connection ready, then immediately correct view
32
+
33
+ ## Client Usage Pattern
34
+
35
+ Template:
36
+ div v-if="!ready" - loading spinner
37
+ LoginModal v-else-if="!user"
38
+ RouterView v-else
39
+
40
+ ## Suggested Server Changes
41
+
42
+ In src/runtime/ws.ts, modify handleOpen to:
43
+ 1. Parse token from URL query params
44
+ 2. If valid, verify token and fetch user profile
45
+ 3. Send connection:ready message with user (or null)
46
+
47
+ ## Suggested Client Changes
48
+
49
+ In src/client/ws.ts:
50
+ 1. Add user and ready properties to class
51
+ 2. Handle connection:ready message in handleMessage
52
+ 3. Add onReady callback method
53
+
54
+ ## Migration
55
+
56
+ - Existing clients that dont handle connection:ready will ignore it (no breaking change)
57
+ - New clients can opt-in to the pattern
@@ -0,0 +1,111 @@
1
+ # tzql Bug Report
2
+
3
+ Bugs discovered while building the Venues application with tzql.
4
+
5
+ ---
6
+
7
+ ## Bug 1: Hidden Fields Exposed in Subscribables
8
+
9
+ **Severity:** High (Security)
10
+
11
+ **Status:** ✅ FIXED
12
+
13
+ **Description:**
14
+ Fields marked as `hidden: true` in the domain schema are still exposed when using subscribables. The generated SQL uses `row_to_json(root.*)` which includes all columns regardless of the `hidden` property.
15
+
16
+ **Fix:**
17
+ Added `buildVisibleRowJson` helper in `subscribable_sql.ts` that generates `jsonb_build_object()` with only visible fields instead of `row_to_json(root.*)`. Also updated all other SQL generators (`sql.ts`) to exclude hidden fields from `get_*`, `search_*`, `save_*`, and `delete_*` functions.
18
+
19
+ ---
20
+
21
+ ## Bug 2: Generated Pinia Stores Don't Await Async Subscribe
22
+
23
+ **Severity:** Medium
24
+
25
+ **Status:** ✅ FIXED
26
+
27
+ **Description:**
28
+ The generated Pinia stores call the async `subscribe` method but don't await it. This means `store.data` is undefined immediately after calling `store.subscribe()` even though the subscription has been initiated.
29
+
30
+ **Fix:**
31
+ Updated `subscribable_store.ts` to make the `bind` function async. It now:
32
+ 1. Creates a `ready` Promise that resolves when first data arrives
33
+ 2. Awaits the `ready` Promise before returning
34
+ 3. Stores the `ready` Promise so repeat calls can also await it
35
+
36
+ **Generated Code (after fix):**
37
+ ```typescript
38
+ async function bind(params) {
39
+ const key = JSON.stringify(params);
40
+ if (documents.value[key]) {
41
+ const existing = documents.value[key];
42
+ if (existing.loading.value) {
43
+ await existing.ready;
44
+ }
45
+ return existing;
46
+ }
47
+
48
+ const docState = ref(null);
49
+ const loading = ref(true);
50
+ let resolveReady;
51
+ const ready = new Promise((resolve) => { resolveReady = resolve; });
52
+
53
+ ws.api.subscribe_venue_detail(params, (initialData) => {
54
+ docState.value = initialData;
55
+ loading.value = false;
56
+ resolveReady();
57
+ });
58
+
59
+ documents.value[key] = { data: docState, loading, ready };
60
+ await ready;
61
+ return documents.value[key];
62
+ }
63
+ ```
64
+
65
+ **Usage:**
66
+ ```typescript
67
+ const store = useMyProfileStore();
68
+ const { data, loading } = await store.bind({ user_id: 1 });
69
+ // data.value is now populated
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Bug 3: Subscribable Permission Check Uses Param Name Instead of Column Name
75
+
76
+ **Severity:** High (Breaking)
77
+
78
+ **Status:** ✅ FIXED
79
+
80
+ **Description:**
81
+ When generating SQL for subscribable permission checks, the compiler uses the parameter name instead of the actual database column name. This causes SQL errors when the param name differs from the column name.
82
+
83
+ **Fix:**
84
+ Updated `compileSubscribePermission` in `subscribable_sql.ts` to map param names to the root entity's `id` column when they match the rootKey. For example, `@org_id` now correctly resolves to `v_root.id` instead of `v_root.org_id`.
85
+
86
+ ---
87
+
88
+ ## Summary
89
+
90
+ | Bug | Severity | Status | Notes |
91
+ |-----|----------|--------|-------|
92
+ | Hidden fields exposed | High | ✅ Fixed | Uses `jsonb_build_object` to exclude hidden fields |
93
+ | Stores don't await | Medium | ✅ Fixed | `bind()` is now async and awaits first data |
94
+ | Permission param/column mismatch | High | ✅ Fixed | Maps param name to `id` column |
95
+
96
+ ---
97
+
98
+ ## Environment
99
+
100
+ - tzql version: (linked local development version)
101
+ - Database: PostgreSQL 17 (via Docker)
102
+ - Client: Vue 3 + Pinia + TypeScript
103
+ - Runtime: Bun
104
+
105
+ ---
106
+
107
+ ## Related Files
108
+
109
+ Detailed bug documents created in tzql docs:
110
+ - `/packages/tzql/docs/feature-requests/hidden-fields-in-subscribables.md`
111
+ - `/packages/tzql/docs/feature-requests/subscribable-param-key-bug.md`
@@ -0,0 +1,34 @@
1
+ # Bug: Hidden fields exposed in subscribables
2
+
3
+ **Status: IMPLEMENTED**
4
+
5
+ ## Issue
6
+
7
+ Entity hidden fields are not excluded from subscribable queries.
8
+
9
+ ## Example
10
+
11
+ domain.js:
12
+ users: {
13
+ schema: { ... },
14
+ hidden: ["password_hash"], // Should be excluded from all queries
15
+ }
16
+
17
+ But get_my_profile returns:
18
+ {
19
+ "users": {
20
+ "password_hash": "$2a$06$..." // EXPOSED!
21
+ }
22
+ }
23
+
24
+ ## Root Cause
25
+
26
+ The subscribable SQL generator uses row_to_json(root.*) which returns all columns.
27
+
28
+ ## Fix
29
+
30
+ When generating subscribable SQL, exclude hidden fields by using explicit column list.
31
+
32
+ ## Impact
33
+
34
+ Security vulnerability - sensitive data like password hashes exposed to clients.
@@ -0,0 +1,38 @@
1
+ # Bug: Subscribable permission check uses param name instead of column name
2
+
3
+ ## Issue
4
+
5
+ When generating subscribable SQL, the permission check incorrectly uses the param name instead of the actual column name on the root entity.
6
+
7
+ ## Example
8
+
9
+ Domain:
10
+ ```javascript
11
+ org_dashboard: {
12
+ params: { org_id: "int" },
13
+ root: { entity: "organisations", key: "org_id" },
14
+ canSubscribe: ["@org_id->acts_for[org_id=$]{active}.user_id"]
15
+ }
16
+ ```
17
+
18
+ Generated SQL:
19
+ ```sql
20
+ -- WRONG: v_root.org_id doesn't exist on organisations table
21
+ WHERE acts_for.org_id = v_root.org_id
22
+ ```
23
+
24
+ Should be:
25
+ ```sql
26
+ -- CORRECT: organisations.id is the actual column
27
+ WHERE acts_for.org_id = v_root.id
28
+ ```
29
+
30
+ ## Error
31
+
32
+ ```
33
+ ERROR: record "v_root" has no field "org_id"
34
+ ```
35
+
36
+ ## Fix
37
+
38
+ The compiler should resolve `key: "org_id"` to mean "param org_id maps to the root entity's primary key (id)", not "access v_root.org_id".