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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.2.0",
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.0...'"
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