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.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,537 +0,0 @@
|
|
|
1
|
-
# Live Query Subscriptions
|
|
2
|
-
|
|
3
|
-
Live Query Subscriptions (Pattern 1 from vision.md) enable clients to subscribe to denormalized documents and receive real-time updates when the underlying data changes.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
### Architecture Principles
|
|
8
|
-
|
|
9
|
-
- **PostgreSQL-First**: All matching logic is compiled to PostgreSQL functions, not JavaScript
|
|
10
|
-
- **In-Memory Registry**: Server holds active subscriptions in memory for performance
|
|
11
|
-
- **Zero Runtime Interpretation**: All logic pre-compiled during deployment
|
|
12
|
-
- **Denormalized Documents**: Subscribables combine data from multiple tables into client-friendly views
|
|
13
|
-
|
|
14
|
-
### How It Works
|
|
15
|
-
|
|
16
|
-
1. **Define Subscribable**: Register a subscribable with permissions, parameters, and relations
|
|
17
|
-
2. **Compile**: Generate three PostgreSQL functions:
|
|
18
|
-
- `<name>_can_subscribe(user_id, params)` - Permission check
|
|
19
|
-
- `get_<name>(params, user_id)` - Query function
|
|
20
|
-
- `<name>_affected_documents(table, op, old, new)` - Change detection
|
|
21
|
-
3. **Subscribe**: Client calls `ws.api.subscribe_<name>(params, callback)`
|
|
22
|
-
4. **Update**: Database changes trigger NOTIFY → server forwards atomic events → client applies patches locally
|
|
23
|
-
|
|
24
|
-
> **Note**: DZQL uses [Atomic Updates](./atomic-updates.md) for efficient real-time sync. Instead of re-querying the full document on every change, the server forwards the raw event and the client patches its local copy. This reduces network traffic and preserves client-side UI state.
|
|
25
|
-
|
|
26
|
-
## Quick Start
|
|
27
|
-
|
|
28
|
-
### 1. Define a Subscribable
|
|
29
|
-
|
|
30
|
-
Create a SQL file with your subscribable definition:
|
|
31
|
-
|
|
32
|
-
```sql
|
|
33
|
-
-- examples/venue-detail-subscribable.sql
|
|
34
|
-
SELECT dzql.register_subscribable(
|
|
35
|
-
'venue_detail',
|
|
36
|
-
'{"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb,
|
|
37
|
-
'{"venue_id": "int"}'::jsonb,
|
|
38
|
-
'venues',
|
|
39
|
-
'{
|
|
40
|
-
"org": "organisations",
|
|
41
|
-
"sites": {
|
|
42
|
-
"entity": "sites",
|
|
43
|
-
"filter": "venue_id=$venue_id"
|
|
44
|
-
}
|
|
45
|
-
}'::jsonb
|
|
46
|
-
);
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**Parameters:**
|
|
50
|
-
- `name`: Identifier used in API calls (e.g., `venue_detail`)
|
|
51
|
-
- `permission_paths`: Access control using path DSL
|
|
52
|
-
- `param_schema`: Parameters required to subscribe (subscription key)
|
|
53
|
-
- `root_entity`: Primary table
|
|
54
|
-
- `relations`: Related entities to include in the document
|
|
55
|
-
|
|
56
|
-
### 2. Compile and Deploy
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# Compile subscribable to SQL functions
|
|
60
|
-
bun packages/dzql/src/compiler/cli/compile-subscribable.js \
|
|
61
|
-
examples/venue-detail-subscribable.sql \
|
|
62
|
-
> /tmp/venue_detail.sql
|
|
63
|
-
|
|
64
|
-
# Deploy to database
|
|
65
|
-
psql $DATABASE_URL < /tmp/venue_detail.sql
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
This generates three functions:
|
|
69
|
-
- `venue_detail_can_subscribe(user_id, params)`
|
|
70
|
-
- `get_venue_detail(params, user_id)`
|
|
71
|
-
- `venue_detail_affected_documents(table, op, old, new)`
|
|
72
|
-
|
|
73
|
-
### 3. Client Usage
|
|
74
|
-
|
|
75
|
-
```javascript
|
|
76
|
-
import { WebSocketManager } from '@dzql/client';
|
|
77
|
-
|
|
78
|
-
const ws = new WebSocketManager('ws://localhost:3000/ws');
|
|
79
|
-
await ws.connect();
|
|
80
|
-
|
|
81
|
-
// Subscribe to venue updates
|
|
82
|
-
const { data, subscription_id, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
83
|
-
{ venue_id: 123 },
|
|
84
|
-
(updatedData) => {
|
|
85
|
-
console.log('Venue updated:', updatedData);
|
|
86
|
-
// updatedData = { id: 123, name: 'Venue Name', org: {...}, sites: [...] }
|
|
87
|
-
}
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// Initial data is returned immediately
|
|
91
|
-
console.log('Initial venue data:', data);
|
|
92
|
-
|
|
93
|
-
// Later: unsubscribe when done
|
|
94
|
-
await unsubscribe();
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Subscribable Definition
|
|
98
|
-
|
|
99
|
-
### Permission Paths
|
|
100
|
-
|
|
101
|
-
Control who can subscribe using the path DSL:
|
|
102
|
-
|
|
103
|
-
```javascript
|
|
104
|
-
{
|
|
105
|
-
"subscribe": [
|
|
106
|
-
"@org_id->acts_for[org_id=$]{active}.user_id"
|
|
107
|
-
]
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
This allows users who:
|
|
112
|
-
- Have an active `acts_for` relationship
|
|
113
|
-
- Where `org_id` matches the venue's `org_id`
|
|
114
|
-
|
|
115
|
-
Multiple paths can be provided for OR logic:
|
|
116
|
-
|
|
117
|
-
```javascript
|
|
118
|
-
{
|
|
119
|
-
"subscribe": [
|
|
120
|
-
"@owner_id", // Direct owner
|
|
121
|
-
"@org_id->acts_for[org_id=$]{active}.user_id" // OR org member
|
|
122
|
-
]
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Parameter Schema
|
|
127
|
-
|
|
128
|
-
Define the subscription key (what makes each subscription unique):
|
|
129
|
-
|
|
130
|
-
```javascript
|
|
131
|
-
{
|
|
132
|
-
"venue_id": "int"
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Clients must provide these parameters when subscribing.
|
|
137
|
-
|
|
138
|
-
### Relations
|
|
139
|
-
|
|
140
|
-
Include related data in the denormalized document:
|
|
141
|
-
|
|
142
|
-
```javascript
|
|
143
|
-
{
|
|
144
|
-
// Simple relation - include entire related record
|
|
145
|
-
"org": "organisations",
|
|
146
|
-
|
|
147
|
-
// Filtered relation - include sites filtered by venue_id
|
|
148
|
-
"sites": {
|
|
149
|
-
"entity": "sites",
|
|
150
|
-
"filter": "venue_id=$venue_id"
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
// Nested relations
|
|
154
|
-
"org": {
|
|
155
|
-
"entity": "organisations",
|
|
156
|
-
"relations": {
|
|
157
|
-
"users": {
|
|
158
|
-
"entity": "acts_for",
|
|
159
|
-
"filter": "org_id=$org_id AND valid_to IS NULL"
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Generated Functions
|
|
167
|
-
|
|
168
|
-
### 1. Permission Check: `<name>_can_subscribe`
|
|
169
|
-
|
|
170
|
-
```sql
|
|
171
|
-
CREATE FUNCTION venue_detail_can_subscribe(
|
|
172
|
-
p_user_id INT,
|
|
173
|
-
p_params JSONB
|
|
174
|
-
) RETURNS BOOLEAN;
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Returns `true` if the user can subscribe with the given parameters.
|
|
178
|
-
|
|
179
|
-
Called automatically when client subscribes.
|
|
180
|
-
|
|
181
|
-
### 2. Query Function: `get_<name>`
|
|
182
|
-
|
|
183
|
-
```sql
|
|
184
|
-
CREATE FUNCTION get_venue_detail(
|
|
185
|
-
p_params JSONB,
|
|
186
|
-
p_user_id INT
|
|
187
|
-
) RETURNS JSONB;
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Builds the denormalized document from the database.
|
|
191
|
-
|
|
192
|
-
Called:
|
|
193
|
-
- Initially when client subscribes (returns first data)
|
|
194
|
-
- After each change that affects the subscription (returns updated data)
|
|
195
|
-
|
|
196
|
-
### 3. Change Detection: `<name>_affected_documents`
|
|
197
|
-
|
|
198
|
-
```sql
|
|
199
|
-
CREATE FUNCTION venue_detail_affected_documents(
|
|
200
|
-
p_table TEXT,
|
|
201
|
-
p_op TEXT,
|
|
202
|
-
p_old JSONB,
|
|
203
|
-
p_new JSONB
|
|
204
|
-
) RETURNS JSONB[];
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Determines which subscription instances are affected by a database change.
|
|
208
|
-
|
|
209
|
-
Returns array of parameter sets (subscription keys) that need updates.
|
|
210
|
-
|
|
211
|
-
Example:
|
|
212
|
-
```sql
|
|
213
|
-
-- When venue 123 is updated
|
|
214
|
-
SELECT venue_detail_affected_documents(
|
|
215
|
-
'venues',
|
|
216
|
-
'update',
|
|
217
|
-
'{"id": 123, "name": "Old"}'::jsonb,
|
|
218
|
-
'{"id": 123, "name": "New"}'::jsonb
|
|
219
|
-
);
|
|
220
|
-
-- Returns: [{"venue_id": 123}]
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Server Integration
|
|
224
|
-
|
|
225
|
-
The server automatically:
|
|
226
|
-
1. Handles `subscribe_<name>` and `unsubscribe_<name>` RPC calls
|
|
227
|
-
2. Maintains in-memory subscription registry
|
|
228
|
-
3. Listens to database NOTIFY events
|
|
229
|
-
4. Calls `_affected_documents()` to find affected subscriptions
|
|
230
|
-
5. Re-executes `get_<name>()` to get fresh data
|
|
231
|
-
6. Sends updates to subscribed clients
|
|
232
|
-
|
|
233
|
-
No server code changes needed when adding new subscribables!
|
|
234
|
-
|
|
235
|
-
## WebSocket Protocol
|
|
236
|
-
|
|
237
|
-
### Subscribe
|
|
238
|
-
|
|
239
|
-
```json
|
|
240
|
-
{
|
|
241
|
-
"jsonrpc": "2.0",
|
|
242
|
-
"id": 1,
|
|
243
|
-
"method": "subscribe_venue_detail",
|
|
244
|
-
"params": {
|
|
245
|
-
"venue_id": 123
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
**Response:**
|
|
251
|
-
```json
|
|
252
|
-
{
|
|
253
|
-
"jsonrpc": "2.0",
|
|
254
|
-
"id": 1,
|
|
255
|
-
"result": {
|
|
256
|
-
"subscription_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
257
|
-
"data": {
|
|
258
|
-
"id": 123,
|
|
259
|
-
"name": "Venue Name",
|
|
260
|
-
"org": { "id": 1, "name": "Organization" },
|
|
261
|
-
"sites": [...]
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### Updates
|
|
268
|
-
|
|
269
|
-
When data changes, server sends:
|
|
270
|
-
|
|
271
|
-
```json
|
|
272
|
-
{
|
|
273
|
-
"jsonrpc": "2.0",
|
|
274
|
-
"method": "subscription:update",
|
|
275
|
-
"params": {
|
|
276
|
-
"subscription_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
277
|
-
"subscribable": "venue_detail",
|
|
278
|
-
"data": {
|
|
279
|
-
"id": 123,
|
|
280
|
-
"name": "Updated Venue Name",
|
|
281
|
-
"org": { "id": 1, "name": "Organization" },
|
|
282
|
-
"sites": [...]
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Client's callback is invoked automatically with the new data.
|
|
289
|
-
|
|
290
|
-
### Unsubscribe
|
|
291
|
-
|
|
292
|
-
```json
|
|
293
|
-
{
|
|
294
|
-
"jsonrpc": "2.0",
|
|
295
|
-
"id": 2,
|
|
296
|
-
"method": "unsubscribe_venue_detail",
|
|
297
|
-
"params": {
|
|
298
|
-
"venue_id": 123
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Or call the returned `unsubscribe()` function:
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
const { unsubscribe } = await ws.api.subscribe_venue_detail(...);
|
|
307
|
-
await unsubscribe();
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
## Advanced Examples
|
|
311
|
-
|
|
312
|
-
### User Profile with Nested Relations
|
|
313
|
-
|
|
314
|
-
```sql
|
|
315
|
-
SELECT dzql.register_subscribable(
|
|
316
|
-
'user_profile',
|
|
317
|
-
'{"subscribe": ["@id"]}'::jsonb,
|
|
318
|
-
'{"user_id": "int"}'::jsonb,
|
|
319
|
-
'users',
|
|
320
|
-
'{
|
|
321
|
-
"organisations": {
|
|
322
|
-
"entity": "acts_for",
|
|
323
|
-
"filter": "user_id=$user_id AND valid_to IS NULL",
|
|
324
|
-
"relations": {
|
|
325
|
-
"org": "organisations"
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
"permissions": {
|
|
329
|
-
"entity": "user_permissions",
|
|
330
|
-
"filter": "user_id=$user_id"
|
|
331
|
-
}
|
|
332
|
-
}'::jsonb
|
|
333
|
-
);
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### Multi-Parameter Subscription
|
|
337
|
-
|
|
338
|
-
```sql
|
|
339
|
-
SELECT dzql.register_subscribable(
|
|
340
|
-
'booking_detail',
|
|
341
|
-
'{"subscribe": ["@user_id", "@venue_id->venues.org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb,
|
|
342
|
-
'{"booking_id": "int", "venue_id": "int"}'::jsonb,
|
|
343
|
-
'bookings',
|
|
344
|
-
'{
|
|
345
|
-
"venue": "venues",
|
|
346
|
-
"customer": "users",
|
|
347
|
-
"items": {
|
|
348
|
-
"entity": "booking_items",
|
|
349
|
-
"filter": "booking_id=$booking_id"
|
|
350
|
-
}
|
|
351
|
-
}'::jsonb
|
|
352
|
-
);
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
## Performance Considerations
|
|
356
|
-
|
|
357
|
-
### In-Memory Registry
|
|
358
|
-
|
|
359
|
-
Active subscriptions are stored in-memory on the server:
|
|
360
|
-
- Fast lookup without database queries
|
|
361
|
-
- Automatically cleaned up when WebSocket closes
|
|
362
|
-
- Scale by adding more server instances (subscriptions are connection-local)
|
|
363
|
-
|
|
364
|
-
### Change Detection
|
|
365
|
-
|
|
366
|
-
The `_affected_documents()` function runs for every database change:
|
|
367
|
-
- Keep logic simple and indexed
|
|
368
|
-
- Return only truly affected subscriptions
|
|
369
|
-
- Use early returns for unrelated tables
|
|
370
|
-
|
|
371
|
-
Example optimization:
|
|
372
|
-
|
|
373
|
-
```sql
|
|
374
|
-
CREATE FUNCTION my_subscribable_affected_documents(...)
|
|
375
|
-
RETURNS JSONB[] AS $$
|
|
376
|
-
BEGIN
|
|
377
|
-
-- Early return for unrelated tables
|
|
378
|
-
IF p_table NOT IN ('venues', 'sites') THEN
|
|
379
|
-
RETURN ARRAY[]::JSONB[];
|
|
380
|
-
END IF;
|
|
381
|
-
|
|
382
|
-
-- Use indexed fields
|
|
383
|
-
IF p_table = 'venues' THEN
|
|
384
|
-
RETURN ARRAY[jsonb_build_object('venue_id', (p_new->>'id')::int)];
|
|
385
|
-
END IF;
|
|
386
|
-
|
|
387
|
-
-- ... more logic
|
|
388
|
-
END;
|
|
389
|
-
$$ LANGUAGE plpgsql STABLE;
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Query Efficiency
|
|
393
|
-
|
|
394
|
-
The `get_<name>()` function runs on every update:
|
|
395
|
-
- Use JOINs and indexes appropriately
|
|
396
|
-
- Consider materialized views for complex aggregations
|
|
397
|
-
- Limit relation depth to avoid N+1 queries
|
|
398
|
-
|
|
399
|
-
## Debugging
|
|
400
|
-
|
|
401
|
-
### Check Active Subscriptions
|
|
402
|
-
|
|
403
|
-
```javascript
|
|
404
|
-
// Server-side (in development)
|
|
405
|
-
import { getAllSubscriptions, getStats } from './server/subscriptions.js';
|
|
406
|
-
|
|
407
|
-
console.log('Active subscriptions:', getAllSubscriptions());
|
|
408
|
-
console.log('Stats:', getStats());
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### Test Functions Manually
|
|
412
|
-
|
|
413
|
-
```sql
|
|
414
|
-
-- Test permission check
|
|
415
|
-
SELECT venue_detail_can_subscribe(1, '{"venue_id": 123}'::jsonb);
|
|
416
|
-
|
|
417
|
-
-- Test query
|
|
418
|
-
SELECT get_venue_detail('{"venue_id": 123}'::jsonb, 1);
|
|
419
|
-
|
|
420
|
-
-- Test change detection
|
|
421
|
-
SELECT venue_detail_affected_documents(
|
|
422
|
-
'venues',
|
|
423
|
-
'update',
|
|
424
|
-
'{"id": 123}'::jsonb,
|
|
425
|
-
'{"id": 123, "name": "New Name"}'::jsonb
|
|
426
|
-
);
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
### Enable Debug Logging
|
|
430
|
-
|
|
431
|
-
```javascript
|
|
432
|
-
// Server logs subscription events
|
|
433
|
-
import { wsLogger } from './server/logger.js';
|
|
434
|
-
|
|
435
|
-
wsLogger.level = 'debug'; // See all subscription operations
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
## Migration Guide
|
|
439
|
-
|
|
440
|
-
### From Polling
|
|
441
|
-
|
|
442
|
-
Before:
|
|
443
|
-
```javascript
|
|
444
|
-
// Poll every 5 seconds
|
|
445
|
-
setInterval(async () => {
|
|
446
|
-
const venue = await fetch(`/api/venues/${venueId}`).then(r => r.json());
|
|
447
|
-
updateUI(venue);
|
|
448
|
-
}, 5000);
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
After:
|
|
452
|
-
```javascript
|
|
453
|
-
// Real-time updates
|
|
454
|
-
const { data, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
455
|
-
{ venue_id: venueId },
|
|
456
|
-
(venue) => updateUI(venue)
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
updateUI(data); // Initial data
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### From Pattern 2 (Need to Know)
|
|
463
|
-
|
|
464
|
-
Pattern 2 notifications tell you "something changed":
|
|
465
|
-
```javascript
|
|
466
|
-
ws.onBroadcast('venues:updated', (params) => {
|
|
467
|
-
// Manually fetch updated data
|
|
468
|
-
refetchVenue(params.id);
|
|
469
|
-
});
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
Pattern 1 subscriptions give you the data:
|
|
473
|
-
```javascript
|
|
474
|
-
ws.api.subscribe_venue_detail(
|
|
475
|
-
{ venue_id: 123 },
|
|
476
|
-
(venue) => {
|
|
477
|
-
// Fresh data automatically provided
|
|
478
|
-
updateUI(venue);
|
|
479
|
-
}
|
|
480
|
-
);
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
## Best Practices
|
|
484
|
-
|
|
485
|
-
1. **One subscribable per use case**: Create focused subscribables for specific UI needs
|
|
486
|
-
2. **Minimize relations**: Only include data the client actually needs
|
|
487
|
-
3. **Use specific parameters**: Subscription keys should be precise (e.g., `venue_id`, not `org_id`)
|
|
488
|
-
4. **Clean up subscriptions**: Always unsubscribe when component unmounts
|
|
489
|
-
5. **Handle reconnection**: Client automatically re-authenticates, but may need to re-subscribe
|
|
490
|
-
6. **Test permissions thoroughly**: Use path DSL carefully to prevent unauthorized access
|
|
491
|
-
|
|
492
|
-
## Troubleshooting
|
|
493
|
-
|
|
494
|
-
### Subscription Not Receiving Updates
|
|
495
|
-
|
|
496
|
-
1. Check that `_affected_documents()` returns correct parameter sets:
|
|
497
|
-
```sql
|
|
498
|
-
SELECT my_subscribable_affected_documents('table', 'update', old, new);
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
2. Verify subscription is registered:
|
|
502
|
-
```javascript
|
|
503
|
-
console.log('Subscriptions:', ws.subscriptions.size);
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
3. Confirm WebSocket is connected:
|
|
507
|
-
```javascript
|
|
508
|
-
console.log('Connected:', ws.socket?.readyState === 1);
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
### Permission Denied
|
|
512
|
-
|
|
513
|
-
1. Test permission function directly:
|
|
514
|
-
```sql
|
|
515
|
-
SELECT my_subscribable_can_subscribe(user_id, params);
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
2. Check path DSL syntax in subscribable definition
|
|
519
|
-
|
|
520
|
-
3. Verify user has required relationships (e.g., `acts_for` records)
|
|
521
|
-
|
|
522
|
-
### Compilation Errors
|
|
523
|
-
|
|
524
|
-
1. Validate JSON syntax in subscribable definition
|
|
525
|
-
2. Check that all referenced tables exist
|
|
526
|
-
3. Ensure parameter names match between schema and filters
|
|
527
|
-
4. Test parser separately:
|
|
528
|
-
```bash
|
|
529
|
-
bun packages/dzql/tests/subscriptions/test-subscribable-parse.js
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
## See Also
|
|
533
|
-
|
|
534
|
-
- [Vision Document](../../../../vision.md) - Architecture overview and patterns
|
|
535
|
-
- [Permission Paths](../../../../docs/architecture/PERMISSIONS.md) - Permission path DSL syntax
|
|
536
|
-
- [Subscription Architecture](../../../../docs/architecture/SUBSCRIPTIONS_STRATEGY.md) - Design decisions
|
|
537
|
-
- [Compiler Documentation](../compiler/) - Code generation and compilation guide
|