dzql 0.1.3 → 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 +21 -6
- package/package.json +4 -3
- package/src/compiler/cli/index.js +174 -0
- package/src/compiler/codegen/graph-rules-codegen.js +259 -0
- package/src/compiler/codegen/notification-codegen.js +232 -0
- package/src/compiler/codegen/operation-codegen.js +555 -0
- package/src/compiler/codegen/permission-codegen.js +310 -0
- package/src/compiler/compiler.js +228 -0
- package/src/compiler/index.js +11 -0
- package/src/compiler/parser/entity-parser.js +299 -0
- package/src/compiler/parser/path-parser.js +290 -0
- package/src/database/migrations/002_functions.sql +39 -2
- package/src/database/migrations/003_operations.sql +10 -0
- package/src/database/migrations/005_entities.sql +112 -0
- package/GETTING_STARTED.md +0 -1104
- package/REFERENCE.md +0 -960
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Path Code Generator
|
|
3
|
+
* Generates PostgreSQL notification resolution functions from path ASTs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PathParser } from '../parser/path-parser.js';
|
|
7
|
+
|
|
8
|
+
export class NotificationCodegen {
|
|
9
|
+
constructor(tableName, notificationPaths) {
|
|
10
|
+
this.tableName = tableName;
|
|
11
|
+
this.notificationPaths = notificationPaths;
|
|
12
|
+
this.parser = new PathParser();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate notification path resolution function
|
|
17
|
+
* @returns {string} SQL for notification function
|
|
18
|
+
*/
|
|
19
|
+
generate() {
|
|
20
|
+
if (!this.notificationPaths || Object.keys(this.notificationPaths).length === 0) {
|
|
21
|
+
return this._generateEmptyFunction();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pathCollectors = [];
|
|
25
|
+
|
|
26
|
+
// Generate SQL for each notification path
|
|
27
|
+
for (const [pathName, paths] of Object.entries(this.notificationPaths)) {
|
|
28
|
+
if (!paths || paths.length === 0) continue;
|
|
29
|
+
|
|
30
|
+
for (const path of paths) {
|
|
31
|
+
const ast = this.parser.parse(path);
|
|
32
|
+
const sql = this._generatePathSQL(ast);
|
|
33
|
+
if (sql) {
|
|
34
|
+
pathCollectors.push(`
|
|
35
|
+
-- ${pathName} notification path
|
|
36
|
+
v_users := v_users || ARRAY(${sql});`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pathSQL = pathCollectors.length > 0
|
|
42
|
+
? pathCollectors.join('\n')
|
|
43
|
+
: ' -- No notification paths configured';
|
|
44
|
+
|
|
45
|
+
return `-- Notification path resolution for ${this.tableName}
|
|
46
|
+
CREATE OR REPLACE FUNCTION _resolve_notification_paths_${this.tableName}(
|
|
47
|
+
p_user_id INT,
|
|
48
|
+
p_record JSONB
|
|
49
|
+
) RETURNS INT[] AS $$
|
|
50
|
+
DECLARE
|
|
51
|
+
v_users INT[] := ARRAY[]::INT[];
|
|
52
|
+
BEGIN
|
|
53
|
+
${pathSQL}
|
|
54
|
+
|
|
55
|
+
-- Return unique user IDs
|
|
56
|
+
RETURN ARRAY(SELECT DISTINCT unnest(v_users));
|
|
57
|
+
END;
|
|
58
|
+
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate empty function (no notifications)
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_generateEmptyFunction() {
|
|
66
|
+
return `-- Notification path resolution for ${this.tableName}
|
|
67
|
+
CREATE OR REPLACE FUNCTION _resolve_notification_paths_${this.tableName}(
|
|
68
|
+
p_user_id INT,
|
|
69
|
+
p_record JSONB
|
|
70
|
+
) RETURNS INT[] AS $$
|
|
71
|
+
BEGIN
|
|
72
|
+
RETURN ARRAY[]::INT[]; -- No notification paths configured
|
|
73
|
+
END;
|
|
74
|
+
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate SQL for a path AST
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
_generatePathSQL(ast) {
|
|
82
|
+
switch (ast.type) {
|
|
83
|
+
case 'empty':
|
|
84
|
+
return null; // No users
|
|
85
|
+
|
|
86
|
+
case 'direct_field':
|
|
87
|
+
return this._generateDirectFieldQuery(ast);
|
|
88
|
+
|
|
89
|
+
case 'traversal':
|
|
90
|
+
return this._generateTraversalQuery(ast);
|
|
91
|
+
|
|
92
|
+
case 'dot_path':
|
|
93
|
+
return this._generateDotPathQuery(ast);
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
console.warn('Unknown AST type for notification:', ast.type);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate direct field query: @owner_id
|
|
103
|
+
* Returns single user ID
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
_generateDirectFieldQuery(ast) {
|
|
107
|
+
return `
|
|
108
|
+
SELECT (p_record->>'${ast.field}')::int
|
|
109
|
+
WHERE (p_record->>'${ast.field}') IS NOT NULL
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generate traversal query: @org_id->acts_for[org_id=$]{active}.user_id
|
|
115
|
+
* Returns array of user IDs
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
_generateTraversalQuery(ast) {
|
|
119
|
+
const steps = ast.steps;
|
|
120
|
+
|
|
121
|
+
// Extract components from the path
|
|
122
|
+
let sourceField = null;
|
|
123
|
+
let targetTable = null;
|
|
124
|
+
let targetField = null;
|
|
125
|
+
let filters = [];
|
|
126
|
+
let temporal = false;
|
|
127
|
+
|
|
128
|
+
for (const step of steps) {
|
|
129
|
+
if (step.type === 'field_ref') {
|
|
130
|
+
if (!sourceField) {
|
|
131
|
+
sourceField = step.field;
|
|
132
|
+
} else {
|
|
133
|
+
targetField = step.field;
|
|
134
|
+
}
|
|
135
|
+
} else if (step.type === 'table_ref') {
|
|
136
|
+
targetTable = step.table;
|
|
137
|
+
|
|
138
|
+
if (step.filter) {
|
|
139
|
+
filters = step.filter;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (step.temporal) {
|
|
143
|
+
temporal = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (step.targetField) {
|
|
147
|
+
targetField = step.targetField;
|
|
148
|
+
}
|
|
149
|
+
} else if (step.type === 'dot_path') {
|
|
150
|
+
// Handle dot path: posts.author_id
|
|
151
|
+
targetTable = step.fields[0];
|
|
152
|
+
targetField = step.fields[step.fields.length - 1];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Build WHERE conditions
|
|
157
|
+
const conditions = [];
|
|
158
|
+
|
|
159
|
+
// Add filter conditions
|
|
160
|
+
for (const filter of filters) {
|
|
161
|
+
if (filter.operator === '=' && filter.value.type === 'param') {
|
|
162
|
+
conditions.push(`${targetTable}.${filter.field} = (p_record->>'${sourceField}')::int`);
|
|
163
|
+
} else if (filter.operator === '=') {
|
|
164
|
+
const value = this._formatValue(filter.value);
|
|
165
|
+
conditions.push(`${targetTable}.${filter.field} = ${value}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Default condition: join on source field
|
|
170
|
+
if (sourceField && targetTable && conditions.length === 0) {
|
|
171
|
+
conditions.push(`${targetTable}.id = (p_record->>'${sourceField}')::int`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Add temporal condition
|
|
175
|
+
if (temporal) {
|
|
176
|
+
conditions.push(`${targetTable}.valid_to IS NULL`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Build WHERE clause
|
|
180
|
+
const whereClause = conditions.length > 0
|
|
181
|
+
? 'WHERE ' + conditions.join('\n AND ')
|
|
182
|
+
: '';
|
|
183
|
+
|
|
184
|
+
return `
|
|
185
|
+
SELECT ${targetTable}.${targetField}
|
|
186
|
+
FROM ${targetTable}
|
|
187
|
+
${whereClause}
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generate dot path query
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
_generateDotPathQuery(ast) {
|
|
196
|
+
const lastField = ast.fields[ast.fields.length - 1];
|
|
197
|
+
return `
|
|
198
|
+
SELECT (p_record->>'${lastField}')::int
|
|
199
|
+
WHERE (p_record->>'${lastField}') IS NOT NULL
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Format a value for SQL
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
_formatValue(value) {
|
|
208
|
+
switch (value.type) {
|
|
209
|
+
case 'literal':
|
|
210
|
+
return `'${value.value}'`;
|
|
211
|
+
case 'number':
|
|
212
|
+
return value.value;
|
|
213
|
+
case 'field':
|
|
214
|
+
return `(p_record->>'${value.value}')`;
|
|
215
|
+
case 'param':
|
|
216
|
+
return '?';
|
|
217
|
+
default:
|
|
218
|
+
return 'NULL';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Generate notification path resolution function for an entity
|
|
225
|
+
* @param {string} tableName - Table name
|
|
226
|
+
* @param {Object} notificationPaths - Notification paths object
|
|
227
|
+
* @returns {string} SQL for notification function
|
|
228
|
+
*/
|
|
229
|
+
export function generateNotificationFunction(tableName, notificationPaths) {
|
|
230
|
+
const codegen = new NotificationCodegen(tableName, notificationPaths);
|
|
231
|
+
return codegen.generate();
|
|
232
|
+
}
|