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 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
+ }
@@ -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, -- Graph rules
50
- jsonb_build_object( -- Notification paths
51
- 'owner', array['@user_id']
52
- ),
53
- jsonb_build_object( -- Permission paths
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 now provides built-in support for many-to-many (M2M) relationships through junction tables. Define the relationship once in your entity configuration, and DZQL handles:
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.1",
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.1...'"
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
- return JSON.parse(str.slice(1, -1).replace(/''/g, "'"));
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
  }