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,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
+ }