dzql 0.1.2 → 0.1.4

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 CHANGED
@@ -4,12 +4,10 @@ PostgreSQL-powered framework with automatic CRUD operations and real-time WebSoc
4
4
 
5
5
  ## Documentation
6
6
 
7
- All documentation is maintained in the repository root:
8
-
9
- - **[README.md](../../README.md)** - Project overview and quick start
10
- - **[GETTING_STARTED.md](GETTING_STARTED.md)** - Complete tutorial with working todo app
11
- - **[REFERENCE.md](REFERENCE.md)** - Complete API reference
12
- - **[CLAUDE.md](../../docs/CLAUDE.md)** - Development guide for AI assistants
7
+ - **[Getting Started Guide](docs/GETTING_STARTED.md)** - Complete tutorial with working todo app
8
+ - **[API Reference](docs/REFERENCE.md)** - Complete API documentation
9
+ - **[Compiler Documentation](docs/compiler/)** - Entity compilation guide and coding standards
10
+ - **[Claude Guide](docs/CLAUDE.md)** - Development guide for AI assistants
13
11
  - **[Venues Example](../venues/)** - Full working application
14
12
 
15
13
  ## Quick Install
@@ -33,6 +31,23 @@ const user = await ws.api.save.users({ name: 'Alice' });
33
31
  const results = await ws.api.search.users({ filters: { name: 'alice' } });
34
32
  ```
35
33
 
34
+ ## DZQL Compiler
35
+
36
+ Transform declarative entity definitions into optimized PostgreSQL stored procedures:
37
+
38
+ ```bash
39
+ # Via CLI
40
+ dzql compile database/init_db/009_venues_domain.sql -o compiled/
41
+
42
+ # Programmatically
43
+ import { DZQLCompiler } from 'dzql/compiler';
44
+
45
+ const compiler = new DZQLCompiler();
46
+ const result = compiler.compileFromSQL(sqlContent);
47
+ ```
48
+
49
+ See **[Compiler Documentation](docs/compiler/)** for complete usage guide, coding standards, and advanced features.
50
+
36
51
  ## License
37
52
 
38
53
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",
@@ -8,7 +8,8 @@
8
8
  ".": "./src/server/index.js",
9
9
  "./client": "./src/client/ws.js",
10
10
  "./server": "./src/server/index.js",
11
- "./db": "./src/server/db.js"
11
+ "./db": "./src/server/db.js",
12
+ "./compiler": "./src/compiler/index.js"
12
13
  },
13
14
  "files": [
14
15
  "src/**/*.js",
@@ -20,13 +21,12 @@
20
21
  ],
21
22
  "scripts": {
22
23
  "test": "bun test",
23
- "prepublishOnly": "echo '✅ Publishing DZQL v0.1.2...'"
24
+ "prepublishOnly": "echo '✅ Publishing DZQL v0.1.4...'"
24
25
  },
25
26
  "dependencies": {
26
27
  "jose": "^6.1.0",
27
28
  "postgres": "^3.4.7"
28
29
  },
29
-
30
30
  "keywords": [
31
31
  "postgresql",
32
32
  "postgres",
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * DZQL Compiler CLI
4
+ * Command-line interface for compiling entity definitions
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { resolve, dirname, basename } from 'path';
9
+ import { DZQLCompiler } from '../compiler.js';
10
+
11
+ const USAGE = `
12
+ DZQL Compiler - Transform entity definitions into PostgreSQL functions
13
+
14
+ Usage:
15
+ dzql-compile <input-file> [options]
16
+
17
+ Options:
18
+ -o, --output <dir> Output directory (default: ./compiled)
19
+ -w, --watch Watch for changes and recompile
20
+ -v, --verbose Verbose output
21
+ -h, --help Show this help message
22
+
23
+ Examples:
24
+ dzql-compile entities/venues.sql
25
+ dzql-compile database/init_db/009_venues_domain.sql -o compiled/
26
+ dzql-compile entities/*.sql -o dist/compiled/
27
+
28
+ Environment Variables:
29
+ DZQL_COMPILER_VERBOSE Enable verbose output
30
+ `;
31
+
32
+ class CLI {
33
+ constructor() {
34
+ this.args = process.argv.slice(2);
35
+ this.options = {
36
+ output: './compiled',
37
+ watch: false,
38
+ verbose: process.env.DZQL_COMPILER_VERBOSE === 'true'
39
+ };
40
+ this.compiler = new DZQLCompiler();
41
+ }
42
+
43
+ run() {
44
+ this.parseArgs();
45
+
46
+ if (this.options.help || this.args.length === 0) {
47
+ console.log(USAGE);
48
+ process.exit(0);
49
+ }
50
+
51
+ const inputFile = this.args[0];
52
+
53
+ if (!existsSync(inputFile)) {
54
+ console.error(`Error: File not found: ${inputFile}`);
55
+ process.exit(1);
56
+ }
57
+
58
+ this.compileFile(inputFile);
59
+
60
+ if (this.options.watch) {
61
+ this.watchFile(inputFile);
62
+ }
63
+ }
64
+
65
+ parseArgs() {
66
+ for (let i = 0; i < this.args.length; i++) {
67
+ const arg = this.args[i];
68
+
69
+ switch (arg) {
70
+ case '-o':
71
+ case '--output':
72
+ this.options.output = this.args[++i];
73
+ break;
74
+
75
+ case '-w':
76
+ case '--watch':
77
+ this.options.watch = true;
78
+ break;
79
+
80
+ case '-v':
81
+ case '--verbose':
82
+ this.options.verbose = true;
83
+ break;
84
+
85
+ case '-h':
86
+ case '--help':
87
+ this.options.help = true;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+
93
+ compileFile(inputFile) {
94
+ try {
95
+ console.log(`\n🔨 Compiling: ${inputFile}`);
96
+
97
+ // Read input file
98
+ const sqlContent = readFileSync(inputFile, 'utf-8');
99
+
100
+ // Compile
101
+ const result = this.compiler.compileFromSQL(sqlContent);
102
+
103
+ // Display results
104
+ console.log(`\n📊 Compilation Summary:`);
105
+ console.log(` Total entities: ${result.summary.total}`);
106
+ console.log(` Successful: ${result.summary.successful}`);
107
+ console.log(` Failed: ${result.summary.failed}`);
108
+
109
+ if (result.errors.length > 0) {
110
+ console.log(`\n❌ Errors:`);
111
+ for (const error of result.errors) {
112
+ console.log(` - ${error.entity}: ${error.error}`);
113
+ }
114
+ }
115
+
116
+ // Write output files
117
+ if (result.results.length > 0) {
118
+ this.writeOutputFiles(result.results);
119
+ }
120
+
121
+ console.log(`\n✅ Compilation complete!\n`);
122
+ } catch (error) {
123
+ console.error(`\n❌ Compilation failed:`, error.message);
124
+ if (this.options.verbose) {
125
+ console.error(error.stack);
126
+ }
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ writeOutputFiles(results) {
132
+ // Ensure output directory exists
133
+ if (!existsSync(this.options.output)) {
134
+ mkdirSync(this.options.output, { recursive: true });
135
+ }
136
+
137
+ console.log(`\n📝 Writing compiled files to: ${this.options.output}`);
138
+
139
+ const checksums = {};
140
+
141
+ for (const result of results) {
142
+ const outputFile = resolve(this.options.output, `${result.tableName}.sql`);
143
+
144
+ // Write SQL file
145
+ writeFileSync(outputFile, result.sql, 'utf-8');
146
+
147
+ // Store checksum
148
+ checksums[result.tableName] = {
149
+ checksum: result.checksum,
150
+ generatedAt: result.generatedAt,
151
+ compilationTime: result.compilationTime
152
+ };
153
+
154
+ console.log(` ✓ ${result.tableName}.sql (${result.checksum.substring(0, 8)}...)`);
155
+ }
156
+
157
+ // Write checksums file
158
+ const checksumsFile = resolve(this.options.output, 'checksums.json');
159
+ writeFileSync(checksumsFile, JSON.stringify(checksums, null, 2), 'utf-8');
160
+
161
+ console.log(` ✓ checksums.json`);
162
+ }
163
+
164
+ watchFile(inputFile) {
165
+ console.log(`\n👀 Watching for changes...`);
166
+
167
+ // TODO: Implement file watching
168
+ console.log(` (Watch mode not yet implemented)`);
169
+ }
170
+ }
171
+
172
+ // Run CLI
173
+ const cli = new CLI();
174
+ cli.run();
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Graph Rules Code Generator
3
+ * Generates PostgreSQL functions for graph rule execution
4
+ */
5
+
6
+ export class GraphRulesCodegen {
7
+ constructor(tableName, graphRules) {
8
+ this.tableName = tableName;
9
+ this.graphRules = graphRules;
10
+ }
11
+
12
+ /**
13
+ * Generate all graph rule functions
14
+ * @returns {string} SQL for graph rule functions
15
+ */
16
+ generate() {
17
+ if (!this.graphRules || Object.keys(this.graphRules).length === 0) {
18
+ return ''; // No functions if no rules
19
+ }
20
+
21
+ const functions = [];
22
+
23
+ // Generate function for each trigger (on_create, on_update, on_delete)
24
+ for (const [trigger, rules] of Object.entries(this.graphRules)) {
25
+ const functionSQL = this._generateTriggerFunction(trigger, rules);
26
+ if (functionSQL) {
27
+ functions.push(functionSQL);
28
+ }
29
+ }
30
+
31
+ return functions.join('\n\n');
32
+ }
33
+
34
+ /**
35
+ * Generate function for a specific trigger
36
+ * @private
37
+ */
38
+ _generateTriggerFunction(trigger, rules) {
39
+ const operation = trigger.replace('on_', ''); // on_create -> create
40
+ const functionName = `_graph_${this.tableName}_${trigger}`;
41
+
42
+ const actionBlocks = [];
43
+
44
+ // Process each rule
45
+ for (const [ruleName, ruleConfig] of Object.entries(rules)) {
46
+ const description = ruleConfig.description || ruleName;
47
+ const actions = Array.isArray(ruleConfig.actions)
48
+ ? ruleConfig.actions
49
+ : (ruleConfig.actions ? [ruleConfig.actions] : []);
50
+
51
+ for (const action of actions) {
52
+ const actionSQL = this._generateAction(action, ruleName, description);
53
+ if (actionSQL) {
54
+ actionBlocks.push(actionSQL);
55
+ }
56
+ }
57
+ }
58
+
59
+ if (actionBlocks.length === 0) {
60
+ return null; // No actions, no function
61
+ }
62
+
63
+ // Determine parameters based on operation - p_user_id ALWAYS FIRST
64
+ const params = operation === 'delete'
65
+ ? `p_user_id INT,\n p_old_record JSONB`
66
+ : operation === 'update'
67
+ ? `p_user_id INT,\n p_old_record JSONB,\n p_new_record JSONB`
68
+ : `p_user_id INT,\n p_record JSONB`;
69
+
70
+ return `-- Graph rules: ${trigger} on ${this.tableName}
71
+ CREATE OR REPLACE FUNCTION ${functionName}(
72
+ ${params}
73
+ ) RETURNS VOID AS $$
74
+ BEGIN
75
+ ${actionBlocks.join('\n\n')}
76
+ END;
77
+ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
78
+ }
79
+
80
+ /**
81
+ * Generate SQL for a single action
82
+ * @private
83
+ */
84
+ _generateAction(action, ruleName, description) {
85
+ const comment = ` -- ${description}`;
86
+
87
+ switch (action.type) {
88
+ case 'create':
89
+ return this._generateCreateAction(action, comment);
90
+
91
+ case 'update':
92
+ return this._generateUpdateAction(action, comment);
93
+
94
+ case 'delete':
95
+ return this._generateDeleteAction(action, comment);
96
+
97
+ case 'validate':
98
+ return this._generateValidateAction(action, comment);
99
+
100
+ case 'execute':
101
+ return this._generateExecuteAction(action, comment);
102
+
103
+ default:
104
+ console.warn('Unknown action type:', action.type);
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Generate CREATE action
111
+ * @private
112
+ */
113
+ _generateCreateAction(action, comment) {
114
+ const entity = action.entity;
115
+ const data = action.data;
116
+
117
+ const fields = [];
118
+ const values = [];
119
+
120
+ for (const [field, value] of Object.entries(data)) {
121
+ fields.push(field);
122
+ values.push(this._resolveValue(value));
123
+ }
124
+
125
+ return `${comment}
126
+ INSERT INTO ${entity} (${fields.join(', ')})
127
+ VALUES (${values.join(', ')});`;
128
+ }
129
+
130
+ /**
131
+ * Generate UPDATE action
132
+ * @private
133
+ */
134
+ _generateUpdateAction(action, comment) {
135
+ const entity = action.entity;
136
+ const data = action.data;
137
+ const match = action.match;
138
+
139
+ const setClauses = [];
140
+ for (const [field, value] of Object.entries(data)) {
141
+ setClauses.push(`${field} = ${this._resolveValue(value)}`);
142
+ }
143
+
144
+ const whereClauses = [];
145
+ for (const [field, value] of Object.entries(match)) {
146
+ whereClauses.push(`${field} = ${this._resolveValue(value)}`);
147
+ }
148
+
149
+ return `${comment}
150
+ UPDATE ${entity}
151
+ SET ${setClauses.join(', ')}
152
+ WHERE ${whereClauses.join(' AND ')};`;
153
+ }
154
+
155
+ /**
156
+ * Generate DELETE action
157
+ * @private
158
+ */
159
+ _generateDeleteAction(action, comment) {
160
+ const entity = action.entity;
161
+ const match = action.match;
162
+
163
+ const whereClauses = [];
164
+ for (const [field, value] of Object.entries(match)) {
165
+ whereClauses.push(`${field} = ${this._resolveValue(value)}`);
166
+ }
167
+
168
+ return `${comment}
169
+ DELETE FROM ${entity}
170
+ WHERE ${whereClauses.join(' AND ')};`;
171
+ }
172
+
173
+ /**
174
+ * Generate VALIDATE action
175
+ * @private
176
+ */
177
+ _generateValidateAction(action, comment) {
178
+ const functionName = action.function;
179
+ const params = action.params || {};
180
+ const errorMessage = action.error_message || 'Validation failed';
181
+
182
+ const paramList = [];
183
+ for (const [key, value] of Object.entries(params)) {
184
+ paramList.push(`${key} => ${this._resolveValue(value)}`);
185
+ }
186
+
187
+ const paramSQL = paramList.length > 0 ? paramList.join(', ') : '';
188
+
189
+ return `${comment}
190
+ IF NOT ${functionName}(${paramSQL}) THEN
191
+ RAISE EXCEPTION '${errorMessage}';
192
+ END IF;`;
193
+ }
194
+
195
+ /**
196
+ * Generate EXECUTE action
197
+ * @private
198
+ */
199
+ _generateExecuteAction(action, comment) {
200
+ const functionName = action.function;
201
+ const params = action.params || {};
202
+
203
+ const paramList = [];
204
+ for (const [key, value] of Object.entries(params)) {
205
+ paramList.push(`${key} => ${this._resolveValue(value)}`);
206
+ }
207
+
208
+ const paramSQL = paramList.length > 0 ? paramList.join(', ') : '';
209
+
210
+ return `${comment}
211
+ PERFORM ${functionName}(${paramSQL});`;
212
+ }
213
+
214
+ /**
215
+ * Resolve a value (variable reference or literal)
216
+ * @private
217
+ */
218
+ _resolveValue(value) {
219
+ if (typeof value !== 'string') {
220
+ // Number or other type
221
+ return value;
222
+ }
223
+
224
+ // Handle special variables
225
+ if (value.startsWith('@')) {
226
+ const varName = value.substring(1);
227
+
228
+ // Special keywords
229
+ switch (varName) {
230
+ case 'user_id':
231
+ return 'p_user_id';
232
+
233
+ case 'today':
234
+ return 'CURRENT_DATE';
235
+
236
+ case 'now':
237
+ return 'NOW()';
238
+
239
+ default:
240
+ // Field reference from record
241
+ return `(p_record->>'${varName}')`;
242
+ }
243
+ }
244
+
245
+ // String literal
246
+ return `'${value}'`;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Generate graph rule functions for an entity
252
+ * @param {string} tableName - Table name
253
+ * @param {Object} graphRules - Graph rules object
254
+ * @returns {string} SQL for graph rule functions
255
+ */
256
+ export function generateGraphRuleFunctions(tableName, graphRules) {
257
+ const codegen = new GraphRulesCodegen(tableName, graphRules);
258
+ return codegen.generate();
259
+ }