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.
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Permission Code Generator
3
+ * Generates PostgreSQL permission check functions from path ASTs
4
+ */
5
+
6
+ import { PathParser } from '../parser/path-parser.js';
7
+
8
+ export class PermissionCodegen {
9
+ constructor(tableName, permissionPaths) {
10
+ this.tableName = tableName;
11
+ this.permissionPaths = permissionPaths;
12
+ this.parser = new PathParser();
13
+ }
14
+
15
+ /**
16
+ * Generate all permission check functions
17
+ * @returns {string} SQL for permission functions
18
+ */
19
+ generate() {
20
+ const functions = [];
21
+
22
+ // Always generate the 4 standard permission functions
23
+ const standardOperations = ['view', 'create', 'update', 'delete'];
24
+
25
+ for (const operation of standardOperations) {
26
+ // Clean the operation name (remove any comments or special characters)
27
+ const cleanOperation = this._cleanOperationName(operation);
28
+
29
+ // Get paths for this operation (checking both clean and original keys)
30
+ const paths = this.permissionPaths[operation]
31
+ || this.permissionPaths[cleanOperation]
32
+ || this._findPathsByPartialMatch(operation);
33
+
34
+ if (!paths || paths.length === 0) {
35
+ // Public access - always returns true
36
+ functions.push(this._generatePublicPermission(cleanOperation));
37
+ } else {
38
+ functions.push(this._generatePermissionFunction(cleanOperation, paths));
39
+ }
40
+ }
41
+
42
+ return functions.join('\n\n');
43
+ }
44
+
45
+ /**
46
+ * Clean operation name - remove comments, quotes, newlines
47
+ * @private
48
+ */
49
+ _cleanOperationName(operation) {
50
+ if (!operation || typeof operation !== 'string') {
51
+ return 'unknown';
52
+ }
53
+
54
+ // Remove SQL comments (-- ... to end of line)
55
+ let clean = operation.replace(/--[^\n]*/g, '');
56
+
57
+ // Remove quotes
58
+ clean = clean.replace(/['"]/g, '');
59
+
60
+ // Remove newlines and extra whitespace
61
+ clean = clean.replace(/\s+/g, ' ').trim();
62
+
63
+ // Extract just the operation name (view, create, update, delete)
64
+ // Match common patterns
65
+ if (clean.includes('view')) return 'view';
66
+ if (clean.includes('create')) return 'create';
67
+ if (clean.includes('update')) return 'update';
68
+ if (clean.includes('delete')) return 'delete';
69
+
70
+ // If no match, return the first word
71
+ const firstWord = clean.split(/\s+/)[0].toLowerCase();
72
+ return firstWord || 'unknown';
73
+ }
74
+
75
+ /**
76
+ * Find paths by partial match in permission keys
77
+ * Handles cases where keys might have comments embedded
78
+ * @private
79
+ */
80
+ _findPathsByPartialMatch(operation) {
81
+ for (const [key, paths] of Object.entries(this.permissionPaths)) {
82
+ const cleanKey = this._cleanOperationName(key);
83
+ if (cleanKey === operation) {
84
+ return paths;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Generate a permission function for an operation
92
+ * @private
93
+ */
94
+ _generatePermissionFunction(operation, paths) {
95
+ const functionName = `can_${operation}_${this.tableName}`;
96
+ const checks = [];
97
+
98
+ for (const path of paths) {
99
+ const ast = this.parser.parse(path);
100
+ const sql = this._generatePathSQL(ast);
101
+ if (sql) {
102
+ checks.push(sql);
103
+ }
104
+ }
105
+
106
+ // Combine checks with OR logic
107
+ const checkSQL = checks.length > 0
108
+ ? checks.join('\n OR ')
109
+ : 'false';
110
+
111
+ return `-- Permission check: ${operation} on ${this.tableName}
112
+ CREATE OR REPLACE FUNCTION can_${operation}_${this.tableName}(
113
+ p_user_id INT,
114
+ p_record JSONB
115
+ ) RETURNS BOOLEAN AS $$
116
+ BEGIN
117
+ RETURN (
118
+ ${checkSQL}
119
+ );
120
+ END;
121
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
122
+ }
123
+
124
+ /**
125
+ * Generate public permission (always true)
126
+ * @private
127
+ */
128
+ _generatePublicPermission(operation) {
129
+ return `-- Permission check: ${operation} on ${this.tableName} (public access)
130
+ CREATE OR REPLACE FUNCTION can_${operation}_${this.tableName}(
131
+ p_user_id INT,
132
+ p_record JSONB
133
+ ) RETURNS BOOLEAN AS $$
134
+ BEGIN
135
+ RETURN true; -- Public access
136
+ END;
137
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
138
+ }
139
+
140
+ /**
141
+ * Generate SQL for a path AST
142
+ * @private
143
+ */
144
+ _generatePathSQL(ast) {
145
+ switch (ast.type) {
146
+ case 'empty':
147
+ return 'true'; // No restriction
148
+
149
+ case 'direct_field':
150
+ return this._generateDirectFieldCheck(ast);
151
+
152
+ case 'traversal':
153
+ return this._generateTraversalCheck(ast);
154
+
155
+ case 'dot_path':
156
+ return this._generateDotPathCheck(ast);
157
+
158
+ default:
159
+ console.warn('Unknown AST type:', ast.type);
160
+ return 'false';
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Generate direct field check: @owner_id
166
+ * @private
167
+ */
168
+ _generateDirectFieldCheck(ast) {
169
+ return `(p_record->>'${ast.field}')::int = p_user_id`;
170
+ }
171
+
172
+ /**
173
+ * Generate traversal check: @org_id->acts_for[org_id=$]{active}.user_id
174
+ * @private
175
+ */
176
+ _generateTraversalCheck(ast) {
177
+ const steps = ast.steps;
178
+
179
+ // Extract components from the path
180
+ let sourceField = null;
181
+ let targetTable = null;
182
+ let targetField = null;
183
+ let filters = [];
184
+ let temporal = false;
185
+
186
+ for (const step of steps) {
187
+ if (step.type === 'field_ref') {
188
+ if (!sourceField) {
189
+ // First field reference is the source
190
+ sourceField = step.field;
191
+ } else {
192
+ // Last field reference is the target
193
+ targetField = step.field;
194
+ }
195
+ } else if (step.type === 'table_ref') {
196
+ targetTable = step.table;
197
+
198
+ // Collect filter conditions
199
+ if (step.filter) {
200
+ filters = step.filter;
201
+ }
202
+
203
+ // Check for temporal marker
204
+ if (step.temporal) {
205
+ temporal = true;
206
+ }
207
+
208
+ // Get target field if specified in table ref
209
+ if (step.targetField) {
210
+ targetField = step.targetField;
211
+ }
212
+ }
213
+ }
214
+
215
+ // Build WHERE conditions
216
+ const conditions = [];
217
+
218
+ // Add filter conditions
219
+ for (const filter of filters) {
220
+ if (filter.operator === '=' && filter.value.type === 'param') {
221
+ // field=$ means match the record's field value
222
+ conditions.push(`${targetTable}.${filter.field} = (p_record->>'${sourceField}')::int`);
223
+ } else if (filter.operator === '=') {
224
+ const value = this._formatValue(filter.value);
225
+ conditions.push(`${targetTable}.${filter.field} = ${value}`);
226
+ }
227
+ }
228
+
229
+ // Add temporal condition
230
+ if (temporal) {
231
+ conditions.push(`${targetTable}.valid_to IS NULL`);
232
+ }
233
+
234
+ // Add user_id check (final target)
235
+ if (targetField) {
236
+ conditions.push(`${targetTable}.${targetField} = p_user_id`);
237
+ }
238
+
239
+ // Build EXISTS query
240
+ const whereClause = conditions.length > 0
241
+ ? 'WHERE ' + conditions.join('\n AND ')
242
+ : '';
243
+
244
+ return `EXISTS (
245
+ SELECT 1 FROM ${targetTable}
246
+ ${whereClause}
247
+ )`;
248
+ }
249
+
250
+ /**
251
+ * Generate filter condition SQL
252
+ * @private
253
+ */
254
+ _generateFilterCondition(condition, tableAlias) {
255
+ const field = `${tableAlias}.${condition.field}`;
256
+ const value = this._formatValue(condition.value);
257
+
258
+ switch (condition.operator) {
259
+ case '=':
260
+ if (condition.value.type === 'param') {
261
+ // Special case: field=$ means use the record's value
262
+ return `${field} = (p_record->>'${condition.field}')::int`;
263
+ }
264
+ return `${field} = ${value}`;
265
+
266
+ default:
267
+ return `${field} ${condition.operator} ${value}`;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Format a value for SQL
273
+ * @private
274
+ */
275
+ _formatValue(value) {
276
+ switch (value.type) {
277
+ case 'literal':
278
+ return `'${value.value}'`;
279
+ case 'number':
280
+ return value.value;
281
+ case 'field':
282
+ return `(p_record->>'${value.value}')`;
283
+ case 'param':
284
+ return '?'; // Will be replaced by caller
285
+ default:
286
+ return 'NULL';
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Generate dot path check (less common)
292
+ * @private
293
+ */
294
+ _generateDotPathCheck(ast) {
295
+ // For now, treat as a field reference to the last field
296
+ const lastField = ast.fields[ast.fields.length - 1];
297
+ return `(p_record->>'${lastField}')::int = p_user_id`;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Generate permission check functions for an entity
303
+ * @param {string} tableName - Table name
304
+ * @param {Object} permissionPaths - Permission paths object
305
+ * @returns {string} SQL for permission functions
306
+ */
307
+ export function generatePermissionFunctions(tableName, permissionPaths) {
308
+ const codegen = new PermissionCodegen(tableName, permissionPaths);
309
+ return codegen.generate();
310
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * DZQL Compiler
3
+ * Main compiler class that orchestrates parsing and code generation
4
+ */
5
+
6
+ import { EntityParser } from './parser/entity-parser.js';
7
+ import { generatePermissionFunctions } from './codegen/permission-codegen.js';
8
+ import { generateOperations } from './codegen/operation-codegen.js';
9
+ import { generateNotificationFunction } from './codegen/notification-codegen.js';
10
+ import { generateGraphRuleFunctions } from './codegen/graph-rules-codegen.js';
11
+ import crypto from 'crypto';
12
+
13
+ export class DZQLCompiler {
14
+ constructor(options = {}) {
15
+ this.options = {
16
+ includeComments: true,
17
+ includeChecksums: true,
18
+ ...options
19
+ };
20
+ this.parser = new EntityParser();
21
+ }
22
+
23
+ /**
24
+ * Compile an entity definition to SQL
25
+ * @param {Object} entity - Entity configuration
26
+ * @returns {Object} Compilation result
27
+ */
28
+ compile(entity) {
29
+ const startTime = Date.now();
30
+
31
+ // Normalize entity configuration
32
+ const normalizedEntity = this.parser.parseFromObject(entity);
33
+
34
+ // Generate SQL sections
35
+ const sections = [];
36
+
37
+ // Header
38
+ if (this.options.includeComments) {
39
+ sections.push(this._generateHeader(normalizedEntity));
40
+ }
41
+
42
+ // Permission functions
43
+ const permissionSQL = generatePermissionFunctions(
44
+ normalizedEntity.tableName,
45
+ normalizedEntity.permissionPaths
46
+ );
47
+ sections.push(permissionSQL);
48
+
49
+ // Operation functions
50
+ const operationSQL = generateOperations(normalizedEntity);
51
+ sections.push(operationSQL);
52
+
53
+ // Notification path resolution (if needed)
54
+ if (normalizedEntity.notificationPaths &&
55
+ Object.keys(normalizedEntity.notificationPaths).length > 0) {
56
+ sections.push(this._generateNotificationFunction(normalizedEntity));
57
+ }
58
+
59
+ // Graph rules (if needed)
60
+ if (normalizedEntity.graphRules &&
61
+ Object.keys(normalizedEntity.graphRules).length > 0) {
62
+ sections.push(this._generateGraphRuleFunctions(normalizedEntity));
63
+ }
64
+
65
+ // Combine all sections
66
+ const sql = sections.join('\n\n');
67
+
68
+ // Calculate checksum
69
+ const checksum = this._calculateChecksum(sql);
70
+
71
+ const result = {
72
+ tableName: normalizedEntity.tableName,
73
+ sql,
74
+ checksum,
75
+ compilationTime: Date.now() - startTime,
76
+ generatedAt: new Date().toISOString()
77
+ };
78
+
79
+ return result;
80
+ }
81
+
82
+ /**
83
+ * Compile multiple entities
84
+ * @param {Array} entities - Array of entity configurations
85
+ * @returns {Object} Compilation results
86
+ */
87
+ compileAll(entities) {
88
+ const results = [];
89
+ const errors = [];
90
+
91
+ for (const entity of entities) {
92
+ try {
93
+ const result = this.compile(entity);
94
+ results.push(result);
95
+ } catch (error) {
96
+ errors.push({
97
+ entity: entity.tableName || 'unknown',
98
+ error: error.message
99
+ });
100
+ }
101
+ }
102
+
103
+ return {
104
+ results,
105
+ errors,
106
+ summary: {
107
+ total: entities.length,
108
+ successful: results.length,
109
+ failed: errors.length
110
+ }
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Compile from SQL file
116
+ * @param {string} sqlContent - SQL file content
117
+ * @returns {Object} Compilation results
118
+ */
119
+ compileFromSQL(sqlContent) {
120
+ const registerCalls = sqlContent.match(/dzql\.register_entity\s*\([\s\S]*?\);/gi);
121
+
122
+ if (!registerCalls) {
123
+ return {
124
+ results: [],
125
+ errors: [],
126
+ summary: { total: 0, successful: 0, failed: 0 }
127
+ };
128
+ }
129
+
130
+ const entities = [];
131
+ for (const call of registerCalls) {
132
+ try {
133
+ const entity = this.parser.parseFromSQL(call);
134
+ entities.push(entity);
135
+ } catch (error) {
136
+ console.warn('Failed to parse entity:', error.message);
137
+ }
138
+ }
139
+
140
+ return this.compileAll(entities);
141
+ }
142
+
143
+ /**
144
+ * Generate file header
145
+ * @private
146
+ */
147
+ _generateHeader(entity) {
148
+ return `-- ============================================================================
149
+ -- DZQL Compiled Functions for: ${entity.tableName}
150
+ -- Generated: ${new Date().toISOString()}
151
+ --
152
+ -- This file was automatically generated by the DZQL Compiler.
153
+ -- Do not edit directly - regenerate from entity definition.
154
+ -- ============================================================================`;
155
+ }
156
+
157
+ /**
158
+ * Generate notification path resolution function
159
+ * @private
160
+ */
161
+ _generateNotificationFunction(entity) {
162
+ return generateNotificationFunction(
163
+ entity.tableName,
164
+ entity.notificationPaths
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Generate graph rule functions
170
+ * @private
171
+ */
172
+ _generateGraphRuleFunctions(entity) {
173
+ return generateGraphRuleFunctions(
174
+ entity.tableName,
175
+ entity.graphRules
176
+ );
177
+ }
178
+
179
+ /**
180
+ * Calculate SHA-256 checksum of SQL
181
+ * @private
182
+ */
183
+ _calculateChecksum(sql) {
184
+ return crypto.createHash('sha256').update(sql).digest('hex');
185
+ }
186
+
187
+ /**
188
+ * Format SQL with proper indentation (basic)
189
+ * @private
190
+ */
191
+ _formatSQL(sql) {
192
+ // Basic formatting - could be enhanced
193
+ return sql.trim();
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Compile a single entity
199
+ * @param {Object} entity - Entity configuration
200
+ * @param {Object} options - Compiler options
201
+ * @returns {Object} Compilation result
202
+ */
203
+ export function compile(entity, options = {}) {
204
+ const compiler = new DZQLCompiler(options);
205
+ return compiler.compile(entity);
206
+ }
207
+
208
+ /**
209
+ * Compile multiple entities
210
+ * @param {Array} entities - Array of entity configurations
211
+ * @param {Object} options - Compiler options
212
+ * @returns {Object} Compilation results
213
+ */
214
+ export function compileAll(entities, options = {}) {
215
+ const compiler = new DZQLCompiler(options);
216
+ return compiler.compileAll(entities);
217
+ }
218
+
219
+ /**
220
+ * Compile from SQL file content
221
+ * @param {string} sqlContent - SQL file content
222
+ * @param {Object} options - Compiler options
223
+ * @returns {Object} Compilation results
224
+ */
225
+ export function compileFromSQL(sqlContent, options = {}) {
226
+ const compiler = new DZQLCompiler(options);
227
+ return compiler.compileFromSQL(sqlContent);
228
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * DZQL Compiler
3
+ * Transforms declarative entity definitions into optimized PostgreSQL stored procedures
4
+ */
5
+
6
+ export { DZQLCompiler, compile, compileAll, compileFromSQL } from './compiler.js';
7
+ export { EntityParser, parseEntitiesFromSQL } from './parser/entity-parser.js';
8
+ export { PathParser, parsePath, parsePaths } from './parser/path-parser.js';
9
+ export { PermissionCodegen, generatePermissionFunctions } from './codegen/permission-codegen.js';
10
+ export { OperationCodegen, generateOperations } from './codegen/operation-codegen.js';
11
+ export { NotificationCodegen, generateNotificationFunction } from './codegen/notification-codegen.js';