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/README.md CHANGED
@@ -1,54 +1,327 @@
1
- # DZQL Documentation
1
+ # DZQL: The Compile-Only Realtime Database Framework
2
2
 
3
- Complete documentation for the DZQL PostgreSQL-powered framework.
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.
4
4
 
5
- ## 📚 Getting Started
5
+ ## Quick Start
6
6
 
7
- New to DZQL? Start here:
7
+ The fastest way to get started is with `bun create`:
8
8
 
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
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
+ ```
12
16
 
13
- ## 📖 Guides
17
+ This creates a full-stack app with Vue/Vite frontend, DZQL server, and PostgreSQL database.
14
18
 
15
- Feature-specific guides and how-tos:
19
+ ## The Problem
16
20
 
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
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.
23
26
 
24
- ## 📘 Reference
27
+ ## The DZQL Solution
25
28
 
26
- Complete API documentation:
29
+ DZQL takes a radically different approach: **Compilation**.
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
+ Instead of a heavy runtime framework, you define your **Domain Schema** (Entities, Relationships, Permissions) in a simple TypeScript configuration. DZQL compiles this definition into:
31
32
 
32
- ### Compiler Documentation
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.
33
36
 
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
37
+ ### Key Features
38
38
 
39
- ## 🤖 For AI Assistants
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.
40
44
 
41
- - **[Claude Guide](for-ai/claude-guide.md)** - Complete guide for AI-assisted DZQL development
45
+ ## Manual Setup
42
46
 
43
- ## 🔗 Quick Links
47
+ If you prefer to set up manually instead of using `bun create dzql`:
44
48
 
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)
49
+ ### 1. Define your Domain (`domain.ts`)
48
50
 
49
- ## Need Help?
51
+ ```typescript
52
+ export const entities = {
53
+ posts: {
54
+ schema: { id: 'serial PRIMARY KEY', title: 'text', author_id: 'int' },
55
+ permissions: { create: ['@author_id == @user_id'] } // Inlined SQL security
56
+ }
57
+ };
50
58
 
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!)
59
+ export const subscribables = {
60
+ post_feed: {
61
+ root: { entity: 'posts' },
62
+ scopeTables: ['posts']
63
+ }
64
+ };
65
+ ```
66
+
67
+ ### 2. Compile
68
+
69
+ ```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
99
+ ```
100
+
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
+ TZQL 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
+ ```
167
+
168
+ ### Basic Usage
169
+
170
+ ```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: {...} }
180
+
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
186
+ ```
187
+
188
+ ### Vue Component Patterns
189
+
190
+ **Pattern 1: Top-level await (recommended 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
+ // Await at top level - component suspends until data arrives
200
+ const { data } = await store.bind({ venue_id: props.venueId });
201
+ </script>
202
+
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
+ ```
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>
242
+ ```
243
+
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
+ The WebSocket is a pure transport layer - it does not manage or cache data. Stores own their data.
282
+
283
+ When data changes in the database:
284
+ 1. PostgreSQL triggers emit atomic events to the `dzql_v2.events` table
285
+ 2. The runtime calls `*_affected_keys()` to find which subscriptions are affected
286
+ 3. Events are broadcast to subscribed clients via WebSocket
287
+ 4. The **store's** `subscription_event` function receives the event
288
+ 5. The store's `applyPatch` function updates its local document in-place
289
+ 6. Vue reactivity updates the UI automatically
290
+
291
+ **No refetching required** - changes are applied incrementally via patching.
292
+
293
+ ### Patch Events
294
+
295
+ The store receives events with this structure:
296
+
297
+ ```typescript
298
+ {
299
+ table: 'sites', // Which table changed
300
+ op: 'insert' | 'update' | 'delete',
301
+ pk: { id: 123 }, // Primary key of affected row
302
+ data: { ... } // Full row data
303
+ }
304
+ ```
305
+
306
+ The generated `applyPatch` function routes events to the correct location in the document graph based on the subscribable's `includes` structure.
307
+
308
+ ## Why "Compile-Only"?
309
+
310
+ By moving complexity to build-time, we get:
311
+ * **Performance:** The database does the heavy lifting.
312
+ * **Correctness:** If it compiles, your permissions and relationships are valid.
313
+ * **Simplicity:** The runtime is tiny and easy to audit.
314
+
315
+ ## Entity Options
316
+
317
+ | Option | Type | Description |
318
+ |--------|------|-------------|
319
+ | `schema` | object | Column definitions with PostgreSQL types |
320
+ | `primaryKey` | string[] | Composite primary key fields (default: `['id']`) |
321
+ | `label` | string | Field used for display/autocomplete |
322
+ | `softDelete` | boolean | Use `deleted_at` instead of hard delete |
323
+ | `managed` | boolean | Set to `false` to skip CRUD generation (for junction tables) |
324
+ | `permissions` | object | Row-level security rules |
325
+ | `graphRules` | object | Side effects on create/update/delete |
326
+ | `manyToMany` | object | M2M relationship definitions |
327
+ | `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".