dzql 0.1.6 → 0.2.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.
- package/README.md +23 -1
- package/docs/LIVE_QUERY_SUBSCRIPTIONS.md +535 -0
- package/docs/LIVE_QUERY_SUBSCRIPTIONS_STRATEGY.md +488 -0
- package/docs/REFERENCE.md +139 -0
- package/docs/SUBSCRIPTIONS_QUICK_START.md +203 -0
- package/package.json +2 -3
- package/src/client/ws.js +87 -2
- package/src/compiler/cli/compile-example.js +33 -0
- package/src/compiler/cli/compile-subscribable.js +43 -0
- package/src/compiler/cli/debug-compile.js +44 -0
- package/src/compiler/cli/debug-parse.js +26 -0
- package/src/compiler/cli/debug-path-parser.js +18 -0
- package/src/compiler/cli/debug-subscribable-parser.js +21 -0
- package/src/compiler/codegen/subscribable-codegen.js +446 -0
- package/src/compiler/compiler.js +115 -0
- package/src/compiler/parser/subscribable-parser.js +242 -0
- package/src/database/migrations/009_subscriptions.sql +230 -0
- package/src/server/index.js +90 -1
- package/src/server/subscriptions.js +209 -0
- package/src/server/ws.js +78 -2
- package/src/client/stores/README.md +0 -95
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Live Query Subscriptions - Quick Start
|
|
2
|
+
|
|
3
|
+
Get up and running with live query subscriptions in 5 minutes.
|
|
4
|
+
|
|
5
|
+
## Step 1: Create a Subscribable (2 min)
|
|
6
|
+
|
|
7
|
+
Create `my_subscribable.sql`:
|
|
8
|
+
|
|
9
|
+
```sql
|
|
10
|
+
SELECT dzql.register_subscribable(
|
|
11
|
+
'venue_detail', -- Name (use in API)
|
|
12
|
+
'{"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb, -- Who can subscribe
|
|
13
|
+
'{"venue_id": "int"}'::jsonb, -- Subscription parameters
|
|
14
|
+
'venues', -- Root table
|
|
15
|
+
'{"org": "organisations", "sites": {"entity": "sites", "filter": "venue_id=$venue_id"}}'::jsonb -- Related data
|
|
16
|
+
);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Step 2: Compile and Deploy (1 min)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Compile to PostgreSQL functions
|
|
23
|
+
bun packages/dzql/src/compiler/cli/compile-subscribable.js my_subscribable.sql | psql $DATABASE_URL
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This creates 3 functions:
|
|
27
|
+
- `venue_detail_can_subscribe(user_id, params)` - permission check
|
|
28
|
+
- `get_venue_detail(params, user_id)` - query builder
|
|
29
|
+
- `venue_detail_affected_documents(table, op, old, new)` - change detector
|
|
30
|
+
|
|
31
|
+
## Step 3: Subscribe from Client (2 min)
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { WebSocketManager } from '@dzql/client';
|
|
35
|
+
|
|
36
|
+
const ws = new WebSocketManager('ws://localhost:3000/ws');
|
|
37
|
+
await ws.connect();
|
|
38
|
+
|
|
39
|
+
// Subscribe - get initial data + live updates
|
|
40
|
+
const { data, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
41
|
+
{ venue_id: 123 },
|
|
42
|
+
(updatedData) => {
|
|
43
|
+
console.log('Venue changed!', updatedData);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
console.log('Initial data:', data);
|
|
48
|
+
|
|
49
|
+
// Later: cleanup
|
|
50
|
+
await unsubscribe();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## That's It!
|
|
54
|
+
|
|
55
|
+
Your client now receives real-time updates whenever:
|
|
56
|
+
- The venue record changes
|
|
57
|
+
- Related organisation changes
|
|
58
|
+
- Related sites change
|
|
59
|
+
|
|
60
|
+
All change detection happens in PostgreSQL - zero configuration needed on the server!
|
|
61
|
+
|
|
62
|
+
## Next Steps
|
|
63
|
+
|
|
64
|
+
- [Full Documentation](./LIVE_QUERY_SUBSCRIPTIONS.md)
|
|
65
|
+
- [Permission Paths Guide](./PATH_DSL.md)
|
|
66
|
+
- [Example Subscribables](../packages/dzql/examples/subscribables/)
|
|
67
|
+
|
|
68
|
+
## Common Patterns
|
|
69
|
+
|
|
70
|
+
### Simple Document (Single Table)
|
|
71
|
+
|
|
72
|
+
```sql
|
|
73
|
+
SELECT dzql.register_subscribable(
|
|
74
|
+
'user_settings',
|
|
75
|
+
'{"subscribe": ["@user_id"]}'::jsonb, -- Only owner
|
|
76
|
+
'{"user_id": "int"}'::jsonb,
|
|
77
|
+
'user_settings',
|
|
78
|
+
'{}'::jsonb -- No relations
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### With One Relation
|
|
83
|
+
|
|
84
|
+
```sql
|
|
85
|
+
SELECT dzql.register_subscribable(
|
|
86
|
+
'booking_summary',
|
|
87
|
+
'{"subscribe": ["@user_id"]}'::jsonb,
|
|
88
|
+
'{"booking_id": "int"}'::jsonb,
|
|
89
|
+
'bookings',
|
|
90
|
+
'{"venue": "venues"}'::jsonb -- Include venue
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### With Filtered Relations
|
|
95
|
+
|
|
96
|
+
```sql
|
|
97
|
+
SELECT dzql.register_subscribable(
|
|
98
|
+
'organisation_dashboard',
|
|
99
|
+
'{"subscribe": ["@id->acts_for[org_id=$]{active}.user_id"]}'::jsonb,
|
|
100
|
+
'{"org_id": "int"}'::jsonb,
|
|
101
|
+
'organisations',
|
|
102
|
+
'{
|
|
103
|
+
"members": {
|
|
104
|
+
"entity": "acts_for",
|
|
105
|
+
"filter": "org_id=$org_id AND valid_to IS NULL"
|
|
106
|
+
},
|
|
107
|
+
"venues": {
|
|
108
|
+
"entity": "venues",
|
|
109
|
+
"filter": "org_id=$org_id"
|
|
110
|
+
}
|
|
111
|
+
}'::jsonb
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Multiple Permission Paths (OR logic)
|
|
116
|
+
|
|
117
|
+
```sql
|
|
118
|
+
SELECT dzql.register_subscribable(
|
|
119
|
+
'venue_admin',
|
|
120
|
+
'{
|
|
121
|
+
"subscribe": [
|
|
122
|
+
"@owner_id", -- Direct owner
|
|
123
|
+
"@org_id->acts_for[org_id=$]{active}.user_id" -- OR org member
|
|
124
|
+
]
|
|
125
|
+
}'::jsonb,
|
|
126
|
+
'{"venue_id": "int"}'::jsonb,
|
|
127
|
+
'venues',
|
|
128
|
+
'{"sites": {"entity": "sites", "filter": "venue_id=$venue_id"}}'::jsonb
|
|
129
|
+
);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Debugging Tips
|
|
133
|
+
|
|
134
|
+
### Test the functions manually:
|
|
135
|
+
|
|
136
|
+
```sql
|
|
137
|
+
-- Check permission
|
|
138
|
+
SELECT venue_detail_can_subscribe(1, '{"venue_id": 123}'::jsonb);
|
|
139
|
+
|
|
140
|
+
-- Get data
|
|
141
|
+
SELECT get_venue_detail('{"venue_id": 123}'::jsonb, 1);
|
|
142
|
+
|
|
143
|
+
-- Test change detection
|
|
144
|
+
SELECT venue_detail_affected_documents(
|
|
145
|
+
'venues',
|
|
146
|
+
'update',
|
|
147
|
+
'{"id": 123}'::jsonb,
|
|
148
|
+
'{"id": 123, "name": "New"}'::jsonb
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Check active subscriptions:
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
// Client-side
|
|
156
|
+
console.log('My subscriptions:', ws.subscriptions.size);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## FAQ
|
|
160
|
+
|
|
161
|
+
**Q: When should I use subscriptions vs. simple queries?**
|
|
162
|
+
A: Use subscriptions when data changes frequently and client needs to stay in sync. Use simple queries for one-time lookups.
|
|
163
|
+
|
|
164
|
+
**Q: What happens when client disconnects?**
|
|
165
|
+
A: Server automatically cleans up all subscriptions for that connection.
|
|
166
|
+
|
|
167
|
+
**Q: Can multiple clients subscribe to the same data?**
|
|
168
|
+
A: Yes! Each subscription is independent. All will receive updates.
|
|
169
|
+
|
|
170
|
+
**Q: How do I update the subscribable definition?**
|
|
171
|
+
A: Re-compile and deploy. The `register_subscribable()` call uses `ON CONFLICT UPDATE`, so it's safe to run repeatedly.
|
|
172
|
+
|
|
173
|
+
**Q: What if the underlying data is deleted?**
|
|
174
|
+
A: The `get_<name>()` function returns `null`. Handle this in your callback:
|
|
175
|
+
```javascript
|
|
176
|
+
(data) => {
|
|
177
|
+
if (!data) {
|
|
178
|
+
console.log('Record was deleted');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
updateUI(data);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Q: How do I subscribe to a list of items?**
|
|
186
|
+
A: Create a subscribable with array parameters or use multiple subscriptions. For dashboard-style views, consider a single subscribable that returns an array.
|
|
187
|
+
|
|
188
|
+
## Performance Tips
|
|
189
|
+
|
|
190
|
+
1. **Index your joins**: Make sure foreign keys are indexed
|
|
191
|
+
2. **Keep _affected_documents() simple**: Early return for unrelated tables
|
|
192
|
+
3. **Limit relation depth**: Avoid deeply nested relations (max 2-3 levels)
|
|
193
|
+
4. **Use specific subscription keys**: `venue_id` is better than `org_id` (fewer false positives)
|
|
194
|
+
5. **Unsubscribe when done**: Always cleanup to free server resources
|
|
195
|
+
|
|
196
|
+
## Architecture Benefits
|
|
197
|
+
|
|
198
|
+
- ✅ **PostgreSQL-First**: All logic in database, not application code
|
|
199
|
+
- ✅ **Zero Configuration**: No server changes needed for new subscribables
|
|
200
|
+
- ✅ **Type Safe**: Compiled functions validated at deploy time
|
|
201
|
+
- ✅ **Efficient**: In-memory registry, PostgreSQL does matching
|
|
202
|
+
- ✅ **Secure**: Permission paths enforced at database level
|
|
203
|
+
- ✅ **Scalable**: Stateless server, can add instances freely
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "PostgreSQL-powered framework with zero boilerplate CRUD operations and real-time WebSocket synchronization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server/index.js",
|
|
@@ -22,13 +22,12 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "bun test",
|
|
25
|
-
"prepublishOnly": "echo '✅ Publishing DZQL v0.1
|
|
25
|
+
"prepublishOnly": "echo '✅ Publishing DZQL v0.2.1...'"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"jose": "^6.1.0",
|
|
29
29
|
"postgres": "^3.4.7"
|
|
30
30
|
},
|
|
31
|
-
|
|
32
31
|
"keywords": [
|
|
33
32
|
"postgresql",
|
|
34
33
|
"postgres",
|
package/src/client/ws.js
CHANGED
|
@@ -59,12 +59,11 @@ class WebSocketManager {
|
|
|
59
59
|
this.pendingRequests = new Map();
|
|
60
60
|
this.broadcastCallbacks = new Set();
|
|
61
61
|
this.sidRequestHandlers = new Set();
|
|
62
|
+
this.subscriptions = new Map(); // subscription_id -> { callback, unsubscribe }
|
|
62
63
|
this.reconnectAttempts = 0;
|
|
63
64
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
64
65
|
this.isShuttingDown = false;
|
|
65
66
|
|
|
66
|
-
// Ad
|
|
67
|
-
|
|
68
67
|
// DZQL nested proxy API - matches server-side db.api pattern
|
|
69
68
|
// Proxy handles both DZQL operations and custom functions
|
|
70
69
|
const dzqlOps = {
|
|
@@ -137,6 +136,18 @@ class WebSocketManager {
|
|
|
137
136
|
if (prop in target) {
|
|
138
137
|
return target[prop];
|
|
139
138
|
}
|
|
139
|
+
// Handle subscribe_* methods specially
|
|
140
|
+
if (prop.startsWith('subscribe_')) {
|
|
141
|
+
return (params = {}, callback) => {
|
|
142
|
+
return this.subscribe(prop, params, callback);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Handle unsubscribe_* methods
|
|
146
|
+
if (prop.startsWith('unsubscribe_')) {
|
|
147
|
+
return (params = {}) => {
|
|
148
|
+
return this.unsubscribe(prop, params);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
140
151
|
// All other properties are treated as custom function calls
|
|
141
152
|
return (params = {}) => {
|
|
142
153
|
return this.call(prop, params);
|
|
@@ -314,6 +325,16 @@ class WebSocketManager {
|
|
|
314
325
|
resolve(message.result);
|
|
315
326
|
}
|
|
316
327
|
} else {
|
|
328
|
+
// Handle subscription updates
|
|
329
|
+
if (message.method === "subscription:update") {
|
|
330
|
+
const { subscription_id, data } = message.params;
|
|
331
|
+
const sub = this.subscriptions.get(subscription_id);
|
|
332
|
+
if (sub && sub.callback) {
|
|
333
|
+
sub.callback(data);
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
317
338
|
// Handle broadcasts and SID requests
|
|
318
339
|
|
|
319
340
|
// Check if this is a SID request from server
|
|
@@ -376,6 +397,70 @@ class WebSocketManager {
|
|
|
376
397
|
});
|
|
377
398
|
}
|
|
378
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Subscribe to a live query
|
|
402
|
+
*
|
|
403
|
+
* @param {string} method - Method name (subscribe_<subscribable>)
|
|
404
|
+
* @param {object} params - Subscription parameters
|
|
405
|
+
* @param {function} callback - Callback function for updates
|
|
406
|
+
* @returns {Promise<{data, subscription_id, unsubscribe}>} Initial data and unsubscribe function
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* const { data, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
410
|
+
* { venue_id: 1 },
|
|
411
|
+
* (updated) => console.log('Updated:', updated)
|
|
412
|
+
* );
|
|
413
|
+
*
|
|
414
|
+
* // Use initial data
|
|
415
|
+
* console.log('Initial:', data);
|
|
416
|
+
*
|
|
417
|
+
* // Later: unsubscribe
|
|
418
|
+
* unsubscribe();
|
|
419
|
+
*/
|
|
420
|
+
async subscribe(method, params = {}, callback) {
|
|
421
|
+
if (!callback || typeof callback !== 'function') {
|
|
422
|
+
throw new Error('Subscribe requires a callback function');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Call server to register subscription
|
|
426
|
+
const result = await this.call(method, params);
|
|
427
|
+
const { subscription_id, data } = result;
|
|
428
|
+
|
|
429
|
+
// Create unsubscribe function
|
|
430
|
+
const unsubscribeFn = async () => {
|
|
431
|
+
const unsubMethod = method.replace('subscribe_', 'unsubscribe_');
|
|
432
|
+
await this.call(unsubMethod, params);
|
|
433
|
+
this.subscriptions.delete(subscription_id);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Store callback for updates
|
|
437
|
+
this.subscriptions.set(subscription_id, {
|
|
438
|
+
callback,
|
|
439
|
+
unsubscribe: unsubscribeFn
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Return initial data and unsubscribe function
|
|
443
|
+
return {
|
|
444
|
+
data,
|
|
445
|
+
subscription_id,
|
|
446
|
+
unsubscribe: unsubscribeFn
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Unsubscribe from a live query
|
|
452
|
+
*
|
|
453
|
+
* @param {string} method - Method name (unsubscribe_<subscribable>)
|
|
454
|
+
* @param {object} params - Subscription parameters
|
|
455
|
+
* @returns {Promise<{success: boolean}>}
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* await ws.api.unsubscribe_venue_detail({ venue_id: 1 });
|
|
459
|
+
*/
|
|
460
|
+
async unsubscribe(method, params = {}) {
|
|
461
|
+
return await this.call(method, params);
|
|
462
|
+
}
|
|
463
|
+
|
|
379
464
|
/**
|
|
380
465
|
* Register callback for real-time broadcast events
|
|
381
466
|
*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { compileSubscribablesFromSQL } from '../compiler.js';
|
|
5
|
+
|
|
6
|
+
const sqlContent = readFileSync('./examples/subscribables/venue_detail_simple.sql', 'utf-8');
|
|
7
|
+
|
|
8
|
+
console.log('Compiling subscribable...\n');
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const result = compileSubscribablesFromSQL(sqlContent);
|
|
12
|
+
|
|
13
|
+
console.log('Summary:', result.summary);
|
|
14
|
+
|
|
15
|
+
if (result.errors.length > 0) {
|
|
16
|
+
console.log('\nErrors:');
|
|
17
|
+
result.errors.forEach(err => {
|
|
18
|
+
console.log(` ${err.subscribable}: ${err.error}`);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (result.results.length > 0) {
|
|
23
|
+
const compiled = result.results[0];
|
|
24
|
+
console.log(`\n✓ Compiled '${compiled.name}' successfully!`);
|
|
25
|
+
console.log(` Checksum: ${compiled.checksum.substring(0, 16)}...`);
|
|
26
|
+
console.log(` Time: ${compiled.compilationTime}ms`);
|
|
27
|
+
console.log('\nGenerated SQL:\n');
|
|
28
|
+
console.log(compiled.sql);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Error:', error.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compile subscribable and output SQL only (for piping to psql)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { compileSubscribablesFromSQL } from '../compiler.js';
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
if (args.length === 0) {
|
|
12
|
+
console.error('Usage: compile-subscribable.js <sql-file>');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sqlFile = args[0];
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const sqlContent = readFileSync(sqlFile, 'utf-8');
|
|
20
|
+
const result = compileSubscribablesFromSQL(sqlContent);
|
|
21
|
+
|
|
22
|
+
if (result.errors.length > 0) {
|
|
23
|
+
console.error('Compilation errors:');
|
|
24
|
+
result.errors.forEach(err => {
|
|
25
|
+
console.error(` ${err.subscribable}: ${err.error}`);
|
|
26
|
+
});
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (result.results.length === 0) {
|
|
31
|
+
console.error('No subscribables found in', sqlFile);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Output just the SQL
|
|
36
|
+
for (const compiled of result.results) {
|
|
37
|
+
console.log(compiled.sql);
|
|
38
|
+
console.log(''); // Blank line between subscribables
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Error:', error.message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test script for subscribable compilation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { compileSubscribablesFromSQL } from './src/compiler/compiler.js';
|
|
9
|
+
|
|
10
|
+
// Read the example subscribable
|
|
11
|
+
const sqlContent = readFileSync('./examples/subscribables/venue_detail_subscribable.sql', 'utf-8');
|
|
12
|
+
|
|
13
|
+
console.log('Compiling subscribable...\n');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = compileSubscribablesFromSQL(sqlContent);
|
|
17
|
+
|
|
18
|
+
console.log('Compilation Summary:');
|
|
19
|
+
console.log(` Total: ${result.summary.total}`);
|
|
20
|
+
console.log(` Successful: ${result.summary.successful}`);
|
|
21
|
+
console.log(` Failed: ${result.summary.failed}\n`);
|
|
22
|
+
|
|
23
|
+
if (result.errors.length > 0) {
|
|
24
|
+
console.log('Errors:');
|
|
25
|
+
result.errors.forEach(err => {
|
|
26
|
+
console.log(` - ${err.subscribable}: ${err.error}`);
|
|
27
|
+
});
|
|
28
|
+
console.log('');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (result.results.length > 0) {
|
|
32
|
+
const compiled = result.results[0];
|
|
33
|
+
console.log(`Generated SQL for '${compiled.name}':`);
|
|
34
|
+
console.log('='.repeat(80));
|
|
35
|
+
console.log(compiled.sql);
|
|
36
|
+
console.log('='.repeat(80));
|
|
37
|
+
console.log(`\nChecksum: ${compiled.checksum}`);
|
|
38
|
+
console.log(`Compilation time: ${compiled.compilationTime}ms`);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Compilation failed:', error.message);
|
|
42
|
+
console.error(error.stack);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test script for subscribable parsing (debug)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { SubscribableParser } from './src/compiler/parser/subscribable-parser.js';
|
|
9
|
+
|
|
10
|
+
// Read the example subscribable
|
|
11
|
+
const sqlContent = readFileSync('./examples/subscribables/venue_detail_subscribable.sql', 'utf-8');
|
|
12
|
+
|
|
13
|
+
console.log('Parsing subscribable...\n');
|
|
14
|
+
|
|
15
|
+
const parser = new SubscribableParser();
|
|
16
|
+
const subscribables = parser.parseAllFromSQL(sqlContent);
|
|
17
|
+
|
|
18
|
+
console.log('Found', subscribables.length, 'subscribables\n');
|
|
19
|
+
|
|
20
|
+
for (const sub of subscribables) {
|
|
21
|
+
console.log('Subscribable:', sub.name);
|
|
22
|
+
console.log('Root Entity:', sub.rootEntity);
|
|
23
|
+
console.log('Permission Paths:', JSON.stringify(sub.permissionPaths, null, 2));
|
|
24
|
+
console.log('Param Schema:', JSON.stringify(sub.paramSchema, null, 2));
|
|
25
|
+
console.log('Relations:', JSON.stringify(sub.relations, null, 2));
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { PathParser } from './src/compiler/parser/path-parser.js';
|
|
4
|
+
|
|
5
|
+
const parser = new PathParser();
|
|
6
|
+
|
|
7
|
+
const testPath = '@org_id->acts_for[org_id=$]{active}.user_id';
|
|
8
|
+
|
|
9
|
+
console.log('Parsing path:', testPath);
|
|
10
|
+
console.log('');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const ast = parser.parse(testPath);
|
|
14
|
+
console.log('AST:', JSON.stringify(ast, null, 2));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error('Error:', error.message);
|
|
17
|
+
console.error(error.stack);
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { SubscribableParser } from '../parser/subscribable-parser.js';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
const sqlContent = readFileSync('./examples/subscribables/venue_detail_simple.sql', 'utf-8');
|
|
7
|
+
|
|
8
|
+
console.log('Parsing subscribable...\n');
|
|
9
|
+
|
|
10
|
+
const parser = new SubscribableParser();
|
|
11
|
+
const subscribables = parser.parseAllFromSQL(sqlContent);
|
|
12
|
+
|
|
13
|
+
console.log('Found:', subscribables.length, 'subscribables\n');
|
|
14
|
+
|
|
15
|
+
for (const sub of subscribables) {
|
|
16
|
+
console.log('Name:', sub.name);
|
|
17
|
+
console.log('Root Entity:', sub.rootEntity);
|
|
18
|
+
console.log('Permission Paths:', JSON.stringify(sub.permissionPaths, null, 2));
|
|
19
|
+
console.log('Param Schema:', JSON.stringify(sub.paramSchema, null, 2));
|
|
20
|
+
console.log('Relations:', JSON.stringify(sub.relations, null, 2));
|
|
21
|
+
}
|