dzql 0.3.1 ā 0.3.3
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/bin/cli.js +296 -0
- package/docs/compiler/README.md +50 -11
- package/docs/guides/many-to-many.md +19 -1
- package/package.json +3 -2
- package/src/compiler/parser/entity-parser.js +7 -2
package/bin/cli.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { DZQLCompiler } from '../src/compiler/compiler.js';
|
|
6
|
+
|
|
7
|
+
const command = process.argv[2];
|
|
8
|
+
const args = process.argv.slice(3);
|
|
9
|
+
|
|
10
|
+
switch (command) {
|
|
11
|
+
case 'create':
|
|
12
|
+
console.log('š§ Create command coming soon');
|
|
13
|
+
console.log(`Would create project: ${args[0]}`);
|
|
14
|
+
break;
|
|
15
|
+
case 'dev':
|
|
16
|
+
console.log('š§ Dev command coming soon');
|
|
17
|
+
break;
|
|
18
|
+
case 'db:up':
|
|
19
|
+
console.log('š§ Database commands coming soon');
|
|
20
|
+
break;
|
|
21
|
+
case 'compile':
|
|
22
|
+
await runCompile(args);
|
|
23
|
+
break;
|
|
24
|
+
case '--version':
|
|
25
|
+
case '-v':
|
|
26
|
+
const pkg = await import('../package.json', { assert: { type: 'json' } });
|
|
27
|
+
console.log(pkg.default.version);
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
console.log(`
|
|
31
|
+
DZQL CLI
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
dzql create <app-name> Create a new DZQL application
|
|
35
|
+
dzql dev Start development server
|
|
36
|
+
dzql db:up Start PostgreSQL database
|
|
37
|
+
dzql db:down Stop PostgreSQL database
|
|
38
|
+
dzql compile <input> Compile entity definitions to SQL
|
|
39
|
+
dzql --version Show version
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
dzql create my-venue-app
|
|
43
|
+
dzql dev
|
|
44
|
+
dzql compile database/init_db/009_venues_domain.sql -o compiled/
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runCompile(args) {
|
|
49
|
+
const options = {
|
|
50
|
+
output: './compiled',
|
|
51
|
+
verbose: false
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Parse args
|
|
55
|
+
let inputFile = null;
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
|
|
59
|
+
if (arg === '-o' || arg === '--output') {
|
|
60
|
+
options.output = args[++i];
|
|
61
|
+
} else if (arg === '-v' || arg === '--verbose') {
|
|
62
|
+
options.verbose = true;
|
|
63
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
64
|
+
console.log(`
|
|
65
|
+
DZQL Compiler - Transform entity definitions into PostgreSQL functions
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
dzql compile <input-file> [options]
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
-o, --output <dir> Output directory (default: ./compiled)
|
|
72
|
+
-v, --verbose Verbose output
|
|
73
|
+
-h, --help Show this help message
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
dzql compile entities/venues.sql
|
|
77
|
+
dzql compile database/init_db/009_venues_domain.sql -o compiled/
|
|
78
|
+
`);
|
|
79
|
+
return;
|
|
80
|
+
} else if (!inputFile) {
|
|
81
|
+
inputFile = arg;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!inputFile) {
|
|
86
|
+
console.error('Error: No input file specified');
|
|
87
|
+
console.log('Run "dzql compile --help" for usage information');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!existsSync(inputFile)) {
|
|
92
|
+
console.error(`Error: File not found: ${inputFile}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
console.log(`\nšØ Compiling: ${inputFile}`);
|
|
98
|
+
|
|
99
|
+
// Read input file
|
|
100
|
+
const sqlContent = readFileSync(inputFile, 'utf-8');
|
|
101
|
+
|
|
102
|
+
// Compile
|
|
103
|
+
const compiler = new DZQLCompiler();
|
|
104
|
+
const result = compiler.compileFromSQL(sqlContent);
|
|
105
|
+
|
|
106
|
+
// Display results
|
|
107
|
+
console.log(`\nš Compilation Summary:`);
|
|
108
|
+
console.log(` Total entities: ${result.summary.total}`);
|
|
109
|
+
console.log(` Successful: ${result.summary.successful}`);
|
|
110
|
+
console.log(` Failed: ${result.summary.failed}`);
|
|
111
|
+
|
|
112
|
+
if (result.errors.length > 0) {
|
|
113
|
+
console.log(`\nā Errors:`);
|
|
114
|
+
for (const error of result.errors) {
|
|
115
|
+
console.log(` - ${error.entity}: ${error.error}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Write output files
|
|
120
|
+
if (result.results.length > 0) {
|
|
121
|
+
// Ensure output directory exists
|
|
122
|
+
if (!existsSync(options.output)) {
|
|
123
|
+
mkdirSync(options.output, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`\nš Writing compiled files to: ${options.output}`);
|
|
127
|
+
|
|
128
|
+
// Write core DZQL infrastructure
|
|
129
|
+
const coreSQL = `-- DZQL Core Schema and Events System
|
|
130
|
+
|
|
131
|
+
CREATE SCHEMA IF NOT EXISTS dzql;
|
|
132
|
+
|
|
133
|
+
-- Event Audit Table for real-time notifications
|
|
134
|
+
CREATE TABLE IF NOT EXISTS dzql.events (
|
|
135
|
+
event_id bigserial PRIMARY KEY,
|
|
136
|
+
table_name text NOT NULL,
|
|
137
|
+
op text NOT NULL, -- 'insert', 'update', 'delete'
|
|
138
|
+
pk jsonb NOT NULL, -- primary key of affected record
|
|
139
|
+
before jsonb, -- old values (NULL for insert)
|
|
140
|
+
after jsonb, -- new values (NULL for delete)
|
|
141
|
+
user_id int, -- who made the change
|
|
142
|
+
notify_users int[], -- who should be notified
|
|
143
|
+
at timestamptz DEFAULT now() -- when the change occurred
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE INDEX IF NOT EXISTS dzql_events_table_pk_idx ON dzql.events (table_name, pk, at);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS dzql_events_event_id_idx ON dzql.events (event_id);
|
|
148
|
+
|
|
149
|
+
-- Event notification trigger - sends real-time notifications via pg_notify
|
|
150
|
+
CREATE OR REPLACE FUNCTION dzql.notify_event()
|
|
151
|
+
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
|
152
|
+
BEGIN
|
|
153
|
+
PERFORM pg_notify('dzql', jsonb_build_object(
|
|
154
|
+
'event_id', NEW.event_id,
|
|
155
|
+
'table', NEW.table_name,
|
|
156
|
+
'op', NEW.op,
|
|
157
|
+
'pk', NEW.pk,
|
|
158
|
+
'data', COALESCE(NEW.after, NEW.before),
|
|
159
|
+
'before', NEW.before,
|
|
160
|
+
'after', NEW.after,
|
|
161
|
+
'user_id', NEW.user_id,
|
|
162
|
+
'at', NEW.at,
|
|
163
|
+
'notify_users', NEW.notify_users
|
|
164
|
+
)::text);
|
|
165
|
+
|
|
166
|
+
RETURN NULL;
|
|
167
|
+
END $$;
|
|
168
|
+
|
|
169
|
+
DROP TRIGGER IF EXISTS dzql_events_notify ON dzql.events;
|
|
170
|
+
CREATE TRIGGER dzql_events_notify
|
|
171
|
+
AFTER INSERT ON dzql.events
|
|
172
|
+
FOR EACH ROW EXECUTE FUNCTION dzql.notify_event();
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
writeFileSync(resolve(options.output, '000_dzql_core.sql'), coreSQL, 'utf-8');
|
|
176
|
+
console.log(` ā 000_dzql_core.sql`);
|
|
177
|
+
|
|
178
|
+
// Extract schema SQL (everything before DZQL entity registrations)
|
|
179
|
+
const schemaSQL = sqlContent.split(/-- DZQL Entity Registrations|select dzql\.register_entity/i)[0].trim();
|
|
180
|
+
if (schemaSQL) {
|
|
181
|
+
writeFileSync(resolve(options.output, '001_schema.sql'), schemaSQL + '\n', 'utf-8');
|
|
182
|
+
console.log(` ā 001_schema.sql`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Generate auth functions (required for WebSocket server)
|
|
186
|
+
const authSQL = `-- Authentication Functions
|
|
187
|
+
-- Required for DZQL WebSocket server
|
|
188
|
+
|
|
189
|
+
-- Enable pgcrypto extension for password hashing
|
|
190
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
191
|
+
|
|
192
|
+
-- Register new user
|
|
193
|
+
CREATE OR REPLACE FUNCTION register_user(p_email TEXT, p_password TEXT)
|
|
194
|
+
RETURNS JSONB
|
|
195
|
+
LANGUAGE plpgsql
|
|
196
|
+
SECURITY DEFINER
|
|
197
|
+
AS $$
|
|
198
|
+
DECLARE
|
|
199
|
+
user_id INT;
|
|
200
|
+
salt TEXT;
|
|
201
|
+
hash TEXT;
|
|
202
|
+
BEGIN
|
|
203
|
+
-- Generate salt and hash password
|
|
204
|
+
salt := gen_salt('bf', 10);
|
|
205
|
+
hash := crypt(p_password, salt);
|
|
206
|
+
|
|
207
|
+
-- Insert user (assumes users table has: id, email, name, password_hash)
|
|
208
|
+
INSERT INTO users (email, name, password_hash)
|
|
209
|
+
VALUES (p_email, split_part(p_email, '@', 1), hash)
|
|
210
|
+
RETURNING id INTO user_id;
|
|
211
|
+
|
|
212
|
+
RETURN _profile(user_id);
|
|
213
|
+
EXCEPTION
|
|
214
|
+
WHEN unique_violation THEN
|
|
215
|
+
RAISE EXCEPTION 'Email already exists' USING errcode = '23505';
|
|
216
|
+
END $$;
|
|
217
|
+
|
|
218
|
+
-- Login user
|
|
219
|
+
CREATE OR REPLACE FUNCTION login_user(p_email TEXT, p_password TEXT)
|
|
220
|
+
RETURNS JSONB
|
|
221
|
+
LANGUAGE plpgsql
|
|
222
|
+
SECURITY DEFINER
|
|
223
|
+
AS $$
|
|
224
|
+
DECLARE
|
|
225
|
+
user_record RECORD;
|
|
226
|
+
BEGIN
|
|
227
|
+
SELECT id, email, name, password_hash
|
|
228
|
+
INTO user_record
|
|
229
|
+
FROM users
|
|
230
|
+
WHERE email = p_email;
|
|
231
|
+
|
|
232
|
+
IF NOT FOUND THEN
|
|
233
|
+
RAISE EXCEPTION 'Invalid credentials' USING errcode = '28000';
|
|
234
|
+
END IF;
|
|
235
|
+
|
|
236
|
+
IF NOT (user_record.password_hash = crypt(p_password, user_record.password_hash)) THEN
|
|
237
|
+
RAISE EXCEPTION 'Invalid credentials' USING errcode = '28000';
|
|
238
|
+
END IF;
|
|
239
|
+
|
|
240
|
+
RETURN _profile(user_record.id);
|
|
241
|
+
END $$;
|
|
242
|
+
|
|
243
|
+
-- Get user profile (private function, called after login/register)
|
|
244
|
+
CREATE OR REPLACE FUNCTION _profile(p_user_id INT)
|
|
245
|
+
RETURNS JSONB
|
|
246
|
+
LANGUAGE sql
|
|
247
|
+
SECURITY DEFINER
|
|
248
|
+
AS $$
|
|
249
|
+
SELECT jsonb_build_object(
|
|
250
|
+
'user_id', id,
|
|
251
|
+
'email', email,
|
|
252
|
+
'name', name,
|
|
253
|
+
'created_at', created_at
|
|
254
|
+
)
|
|
255
|
+
FROM users
|
|
256
|
+
WHERE id = p_user_id;
|
|
257
|
+
$$;
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
writeFileSync(resolve(options.output, '002_auth.sql'), authSQL, 'utf-8');
|
|
261
|
+
console.log(` ā 002_auth.sql`);
|
|
262
|
+
|
|
263
|
+
const checksums = {};
|
|
264
|
+
|
|
265
|
+
for (const compiledResult of result.results) {
|
|
266
|
+
const outputFile = resolve(options.output, `${compiledResult.tableName}.sql`);
|
|
267
|
+
|
|
268
|
+
// Write SQL file
|
|
269
|
+
writeFileSync(outputFile, compiledResult.sql, 'utf-8');
|
|
270
|
+
|
|
271
|
+
// Store checksum
|
|
272
|
+
checksums[compiledResult.tableName] = {
|
|
273
|
+
checksum: compiledResult.checksum,
|
|
274
|
+
generatedAt: compiledResult.generatedAt,
|
|
275
|
+
compilationTime: compiledResult.compilationTime
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
console.log(` ā ${compiledResult.tableName}.sql (${compiledResult.checksum.substring(0, 8)}...)`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Write checksums file
|
|
282
|
+
const checksumsFile = resolve(options.output, 'checksums.json');
|
|
283
|
+
writeFileSync(checksumsFile, JSON.stringify(checksums, null, 2), 'utf-8');
|
|
284
|
+
|
|
285
|
+
console.log(` ā checksums.json`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log(`\nā
Compilation complete!\n`);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error(`\nā Compilation failed:`, error.message);
|
|
291
|
+
if (options.verbose) {
|
|
292
|
+
console.error(error.stack);
|
|
293
|
+
}
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
package/docs/compiler/README.md
CHANGED
|
@@ -46,17 +46,11 @@ SELECT dzql.register_entity(
|
|
|
46
46
|
array['title', 'description'], -- Searchable fields
|
|
47
47
|
'{}'::jsonb, -- FK includes
|
|
48
48
|
false, -- Soft delete
|
|
49
|
-
'{}'::jsonb, --
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
'view', array['@user_id'],
|
|
55
|
-
'create', array['@user_id'],
|
|
56
|
-
'update', array['@user_id'],
|
|
57
|
-
'delete', array['@user_id']
|
|
58
|
-
),
|
|
59
|
-
'{}'::jsonb -- Temporal config
|
|
49
|
+
'{}'::jsonb, -- Temporal config
|
|
50
|
+
'{}'::jsonb, -- Notification paths
|
|
51
|
+
'{}'::jsonb, -- Permission paths
|
|
52
|
+
'{}'::jsonb, -- Graph rules (including M2M)
|
|
53
|
+
'{}'::jsonb -- Field defaults
|
|
60
54
|
);
|
|
61
55
|
```
|
|
62
56
|
|
|
@@ -67,6 +61,51 @@ This generates 5 PostgreSQL functions:
|
|
|
67
61
|
- `lookup_todos(params, user_id)` - Autocomplete
|
|
68
62
|
- `search_todos(params, user_id)` - Search with filters
|
|
69
63
|
|
|
64
|
+
## Compiler Features (v0.3.1+)
|
|
65
|
+
|
|
66
|
+
The compiler generates **static, optimized SQL** with zero runtime interpretation:
|
|
67
|
+
|
|
68
|
+
### Many-to-Many Relationships
|
|
69
|
+
```sql
|
|
70
|
+
SELECT dzql.register_entity(
|
|
71
|
+
'brands', 'name', ARRAY['name'],
|
|
72
|
+
'{}', false, '{}', '{}', '{}',
|
|
73
|
+
'{
|
|
74
|
+
"many_to_many": {
|
|
75
|
+
"tags": {
|
|
76
|
+
"junction_table": "brand_tags",
|
|
77
|
+
"local_key": "brand_id",
|
|
78
|
+
"foreign_key": "tag_id",
|
|
79
|
+
"target_entity": "tags",
|
|
80
|
+
"id_field": "tag_ids",
|
|
81
|
+
"expand": false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}',
|
|
85
|
+
'{}'
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Generated code:** Static M2M sync blocks (50-100x faster than generic operations)
|
|
90
|
+
- No runtime loops
|
|
91
|
+
- All table/column names are literals
|
|
92
|
+
- PostgreSQL can fully optimize and cache plans
|
|
93
|
+
|
|
94
|
+
See [Many-to-Many Guide](../guides/many-to-many.md) for details.
|
|
95
|
+
|
|
96
|
+
### Field Defaults
|
|
97
|
+
```sql
|
|
98
|
+
'{
|
|
99
|
+
"owner_id": "@user_id",
|
|
100
|
+
"created_at": "@now",
|
|
101
|
+
"status": "draft"
|
|
102
|
+
}'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Generated code:** Auto-populates fields on INSERT
|
|
106
|
+
|
|
107
|
+
See [Field Defaults Guide](../guides/field-defaults.md) for details.
|
|
108
|
+
|
|
70
109
|
## Architecture
|
|
71
110
|
|
|
72
111
|
The compiler uses a three-phase approach:
|
|
@@ -4,7 +4,7 @@ First-class support for many-to-many relationships with automatic junction table
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
DZQL
|
|
7
|
+
DZQL provides built-in support for many-to-many (M2M) relationships through junction tables. Define the relationship once in your entity configuration, and DZQL handles:
|
|
8
8
|
|
|
9
9
|
- Junction table synchronization
|
|
10
10
|
- Atomic updates in single API calls
|
|
@@ -19,6 +19,24 @@ DZQL now provides built-in support for many-to-many (M2M) relationships through
|
|
|
19
19
|
- **Less Boilerplate** - No custom toggle functions needed
|
|
20
20
|
- **Performance Control** - Optional expansion (off by default)
|
|
21
21
|
|
|
22
|
+
## Generic vs Compiled Operations
|
|
23
|
+
|
|
24
|
+
M2M support works in **both** modes:
|
|
25
|
+
|
|
26
|
+
### Generic Operations (Runtime)
|
|
27
|
+
- Uses `dzql.generic_save()` and dynamic SQL
|
|
28
|
+
- Interprets M2M config at runtime (~5-10ms overhead per relationship)
|
|
29
|
+
- Works immediately after `register_entity()` call
|
|
30
|
+
- Good for development and entities with simple M2M
|
|
31
|
+
|
|
32
|
+
### Compiled Operations (v0.3.1+) - RECOMMENDED
|
|
33
|
+
- Generates **static SQL** at compile time
|
|
34
|
+
- **50-100x faster** - zero interpretation overhead
|
|
35
|
+
- All table/column names are literals (PostgreSQL optimizes fully)
|
|
36
|
+
- Recommended for production and complex M2M scenarios
|
|
37
|
+
|
|
38
|
+
See [Compiler Guide](../compiler/README.md) for compilation workflow.
|
|
39
|
+
|
|
22
40
|
## Quick Example
|
|
23
41
|
|
|
24
42
|
### Setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./compiler": "./src/compiler/index.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
+
"bin/**/*.js",
|
|
17
18
|
"src/**/*.js",
|
|
18
19
|
"src/database/migrations/**/*.sql",
|
|
19
20
|
"docs/**/*.md",
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"test": "bun test",
|
|
25
|
-
"prepublishOnly": "echo 'ā
Publishing DZQL v0.3.
|
|
26
|
+
"prepublishOnly": "echo 'ā
Publishing DZQL v0.3.3...'"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"jose": "^6.1.0",
|
|
@@ -176,6 +176,9 @@ export class EntityParser {
|
|
|
176
176
|
_parseJSON(str) {
|
|
177
177
|
if (!str || str === '{}' || str === "'{}'") return {};
|
|
178
178
|
|
|
179
|
+
// Strip SQL comments before parsing
|
|
180
|
+
str = str.replace(/--[^\n]*/g, '').trim();
|
|
181
|
+
|
|
179
182
|
// Handle jsonb_build_object(...) calls
|
|
180
183
|
if (str.includes('jsonb_build_object')) {
|
|
181
184
|
return this._parseJSONBuildObject(str);
|
|
@@ -184,9 +187,11 @@ export class EntityParser {
|
|
|
184
187
|
// Handle JSON string literals
|
|
185
188
|
if (str.startsWith("'") && str.endsWith("'")) {
|
|
186
189
|
try {
|
|
187
|
-
|
|
190
|
+
// Remove outer quotes and unescape SQL quotes
|
|
191
|
+
const jsonStr = str.slice(1, -1).replace(/''/g, "'");
|
|
192
|
+
return JSON.parse(jsonStr);
|
|
188
193
|
} catch (e) {
|
|
189
|
-
console.warn('Failed to parse JSON:', str, e);
|
|
194
|
+
console.warn('Failed to parse JSON:', str.substring(0, 100) + '...', e);
|
|
190
195
|
return {};
|
|
191
196
|
}
|
|
192
197
|
}
|