dzql 0.2.0 → 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 +15 -0
- package/docs/LIVE_QUERY_SUBSCRIPTIONS.md +535 -0
- package/docs/LIVE_QUERY_SUBSCRIPTIONS_STRATEGY.md +488 -0
- package/docs/REFERENCE.md +1 -1
- package/docs/SUBSCRIPTIONS_QUICK_START.md +203 -0
- package/package.json +2 -3
- 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 +52 -2
- package/src/client/stores/README.md +0 -95
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.2.
|
|
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.2.
|
|
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",
|
|
@@ -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
|
+
}
|
|
@@ -78,14 +78,59 @@ END;
|
|
|
78
78
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Check if any path references root entity fields (needs database lookup)
|
|
82
|
+
const needsEntityLookup = subscribePaths.some(path => {
|
|
83
|
+
const ast = this.parser.parse(path);
|
|
84
|
+
return ast.type === 'direct_field' || ast.type === 'field_ref';
|
|
85
|
+
});
|
|
86
|
+
|
|
81
87
|
// Generate permission check logic
|
|
82
88
|
const checks = subscribePaths.map(path => {
|
|
83
89
|
const ast = this.parser.parse(path);
|
|
84
|
-
return this._generatePathCheck(ast, 'p_params', 'p_user_id');
|
|
90
|
+
return this._generatePathCheck(ast, needsEntityLookup ? 'entity' : 'p_params', 'p_user_id');
|
|
85
91
|
});
|
|
86
92
|
|
|
87
93
|
const checkSQL = checks.join(' OR\n ');
|
|
88
94
|
|
|
95
|
+
// If we need entity lookup, fetch it first
|
|
96
|
+
if (needsEntityLookup) {
|
|
97
|
+
const params = Object.keys(this.paramSchema);
|
|
98
|
+
const paramDeclarations = params.map(p => ` v_${p} ${this.paramSchema[p]};`).join('\n');
|
|
99
|
+
const paramExtractions = params.map(p =>
|
|
100
|
+
` v_${p} := (p_params->>'${p}')::${this.paramSchema[p]};`
|
|
101
|
+
).join('\n');
|
|
102
|
+
|
|
103
|
+
const rootFilter = this._generateRootFilter();
|
|
104
|
+
|
|
105
|
+
return `CREATE OR REPLACE FUNCTION ${this.name}_can_subscribe(
|
|
106
|
+
p_user_id INT,
|
|
107
|
+
p_params JSONB
|
|
108
|
+
) RETURNS BOOLEAN AS $$
|
|
109
|
+
DECLARE
|
|
110
|
+
${paramDeclarations}
|
|
111
|
+
entity RECORD;
|
|
112
|
+
BEGIN
|
|
113
|
+
-- Extract parameters
|
|
114
|
+
${paramExtractions}
|
|
115
|
+
|
|
116
|
+
-- Fetch entity
|
|
117
|
+
SELECT * INTO entity
|
|
118
|
+
FROM ${this.rootEntity} root
|
|
119
|
+
WHERE ${rootFilter};
|
|
120
|
+
|
|
121
|
+
-- Entity not found
|
|
122
|
+
IF NOT FOUND THEN
|
|
123
|
+
RETURN FALSE;
|
|
124
|
+
END IF;
|
|
125
|
+
|
|
126
|
+
-- Check permissions
|
|
127
|
+
RETURN (
|
|
128
|
+
${checkSQL}
|
|
129
|
+
);
|
|
130
|
+
END;
|
|
131
|
+
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
|
|
132
|
+
}
|
|
133
|
+
|
|
89
134
|
return `CREATE OR REPLACE FUNCTION ${this.name}_can_subscribe(
|
|
90
135
|
p_user_id INT,
|
|
91
136
|
p_params JSONB
|
|
@@ -104,7 +149,12 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
|
|
|
104
149
|
*/
|
|
105
150
|
_generatePathCheck(ast, recordVar, userIdVar) {
|
|
106
151
|
// Handle direct field reference: @owner_id
|
|
107
|
-
if (ast.type === 'field_ref') {
|
|
152
|
+
if (ast.type === 'direct_field' || ast.type === 'field_ref') {
|
|
153
|
+
// If recordVar is 'entity' (RECORD type), access directly
|
|
154
|
+
if (recordVar === 'entity') {
|
|
155
|
+
return `${recordVar}.${ast.field} = ${userIdVar}`;
|
|
156
|
+
}
|
|
157
|
+
// Otherwise it's p_params (JSONB type)
|
|
108
158
|
return `(${recordVar}->>'${ast.field}')::int = ${userIdVar}`;
|
|
109
159
|
}
|
|
110
160
|
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# DZQL Canonical Pinia Stores
|
|
2
|
-
|
|
3
|
-
**The official, AI-friendly Pinia stores for DZQL Vue.js applications.**
|
|
4
|
-
|
|
5
|
-
## Why These Stores Exist
|
|
6
|
-
|
|
7
|
-
When building DZQL apps, developers (and AI assistants) often struggle with:
|
|
8
|
-
|
|
9
|
-
1. **Three-phase lifecycle** - connecting → login → ready
|
|
10
|
-
2. **WebSocket connection management** - reconnection, error handling
|
|
11
|
-
3. **Authentication flow** - token storage, profile management
|
|
12
|
-
4. **Router integration** - navigation, state synchronization
|
|
13
|
-
5. **Inconsistent patterns** - every project does it differently
|
|
14
|
-
|
|
15
|
-
These canonical stores solve all of these problems with a **simple, consistent pattern** that AI can easily understand and replicate.
|
|
16
|
-
|
|
17
|
-
## The Stores
|
|
18
|
-
|
|
19
|
-
### `useWsStore` - WebSocket & Auth
|
|
20
|
-
|
|
21
|
-
Manages:
|
|
22
|
-
- WebSocket connection (with auto-reconnect)
|
|
23
|
-
- User authentication (login/register/logout)
|
|
24
|
-
- Connection state tracking
|
|
25
|
-
- Three-phase app lifecycle
|
|
26
|
-
|
|
27
|
-
### `useAppStore` - Application State
|
|
28
|
-
|
|
29
|
-
Manages:
|
|
30
|
-
- App initialization
|
|
31
|
-
- Router integration
|
|
32
|
-
- Entity metadata caching
|
|
33
|
-
- Navigation helpers
|
|
34
|
-
- UI state (sidebars, panels)
|
|
35
|
-
|
|
36
|
-
## Quick Example
|
|
37
|
-
|
|
38
|
-
```vue
|
|
39
|
-
<script setup>
|
|
40
|
-
import { computed } from 'vue'
|
|
41
|
-
import { useWsStore, useAppStore } from 'dzql/client/stores'
|
|
42
|
-
|
|
43
|
-
const wsStore = useWsStore()
|
|
44
|
-
const appStore = useAppStore()
|
|
45
|
-
|
|
46
|
-
const state = computed(() => wsStore.appState)
|
|
47
|
-
const ws = wsStore.getWs()
|
|
48
|
-
|
|
49
|
-
// Use DZQL API
|
|
50
|
-
const venues = await ws.api.search.venues({ limit: 50 })
|
|
51
|
-
</script>
|
|
52
|
-
|
|
53
|
-
<template>
|
|
54
|
-
<!-- Three-phase lifecycle -->
|
|
55
|
-
<div v-if="state === 'connecting'">Connecting...</div>
|
|
56
|
-
<LoginView v-else-if="state === 'login'" />
|
|
57
|
-
<MainApp v-else-if="state === 'ready'" />
|
|
58
|
-
</template>
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Documentation
|
|
62
|
-
|
|
63
|
-
- **[Quick Start](../../../docs/CLIENT-QUICK-START.md)** - Get running in 5 minutes
|
|
64
|
-
- **[Complete Guide](../../../docs/CLIENT-STORES.md)** - Full API reference and patterns
|
|
65
|
-
|
|
66
|
-
## Files
|
|
67
|
-
|
|
68
|
-
- `useWsStore.js` - WebSocket connection and authentication
|
|
69
|
-
- `useAppStore.js` - Application state and navigation
|
|
70
|
-
- `index.js` - Exports both stores
|
|
71
|
-
|
|
72
|
-
## For AI Assistants
|
|
73
|
-
|
|
74
|
-
When helping users with DZQL apps:
|
|
75
|
-
|
|
76
|
-
1. **Always use these stores** - Don't create custom connection logic
|
|
77
|
-
2. **Follow the three-phase lifecycle** - connecting → login → ready
|
|
78
|
-
3. **Use computed for reactive state** - `const profile = computed(() => wsStore.profile)`
|
|
79
|
-
4. **Get WS instance for API calls** - `const ws = wsStore.getWs()`
|
|
80
|
-
|
|
81
|
-
**Example prompt for AI:**
|
|
82
|
-
|
|
83
|
-
> "I'm using the canonical DZQL stores from `dzql/client/stores`. The pattern is:
|
|
84
|
-
> 1. useWsStore for WebSocket connection (three phases: connecting, login, ready)
|
|
85
|
-
> 2. useAppStore for app state and navigation
|
|
86
|
-
> 3. Access DZQL API via `wsStore.getWs().api.get.venues({ id: 1 })`
|
|
87
|
-
> Please follow this pattern."
|
|
88
|
-
|
|
89
|
-
## Version
|
|
90
|
-
|
|
91
|
-
These stores are available in DZQL v0.1.6+
|
|
92
|
-
|
|
93
|
-
## License
|
|
94
|
-
|
|
95
|
-
MIT
|