dzql 0.5.33 → 0.6.1

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.
Files changed (142) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +309 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +653 -0
  20. package/docs/project-setup.md +456 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +166 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/runtime/auth.ts +39 -0
  42. package/src/runtime/db.ts +33 -0
  43. package/src/runtime/errors.ts +51 -0
  44. package/src/runtime/index.ts +98 -0
  45. package/src/runtime/js_functions.ts +63 -0
  46. package/src/runtime/manifest_loader.ts +29 -0
  47. package/src/runtime/namespace.ts +483 -0
  48. package/src/runtime/server.ts +87 -0
  49. package/src/runtime/ws.ts +197 -0
  50. package/src/shared/ir.ts +197 -0
  51. package/tests/client.test.ts +38 -0
  52. package/tests/codegen.test.ts +71 -0
  53. package/tests/compiler.test.ts +45 -0
  54. package/tests/graph_rules.test.ts +173 -0
  55. package/tests/integration/db.test.ts +174 -0
  56. package/tests/integration/e2e.test.ts +65 -0
  57. package/tests/integration/features.test.ts +922 -0
  58. package/tests/integration/full_stack.test.ts +262 -0
  59. package/tests/integration/setup.ts +45 -0
  60. package/tests/ir.test.ts +32 -0
  61. package/tests/namespace.test.ts +395 -0
  62. package/tests/permissions.test.ts +55 -0
  63. package/tests/pinia.test.ts +48 -0
  64. package/tests/realtime.test.ts +22 -0
  65. package/tests/runtime.test.ts +80 -0
  66. package/tests/subscribable_gen.test.ts +72 -0
  67. package/tests/subscribable_reactivity.test.ts +258 -0
  68. package/tests/venues_gen.test.ts +25 -0
  69. package/tsconfig.json +20 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/README.md +0 -90
  72. package/bin/cli.js +0 -727
  73. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  74. package/docs/compiler/CODING_STANDARDS.md +0 -415
  75. package/docs/compiler/COMPARISON.md +0 -673
  76. package/docs/compiler/QUICKSTART.md +0 -326
  77. package/docs/compiler/README.md +0 -134
  78. package/docs/examples/README.md +0 -38
  79. package/docs/examples/blog.sql +0 -160
  80. package/docs/examples/venue-detail-simple.sql +0 -8
  81. package/docs/examples/venue-detail-subscribable.sql +0 -45
  82. package/docs/for-ai/claude-guide.md +0 -1210
  83. package/docs/getting-started/quickstart.md +0 -125
  84. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  85. package/docs/getting-started/tutorial.md +0 -1104
  86. package/docs/guides/atomic-updates.md +0 -299
  87. package/docs/guides/client-stores.md +0 -730
  88. package/docs/guides/composite-primary-keys.md +0 -158
  89. package/docs/guides/custom-functions.md +0 -362
  90. package/docs/guides/drop-semantics.md +0 -554
  91. package/docs/guides/field-defaults.md +0 -240
  92. package/docs/guides/interpreter-vs-compiler.md +0 -237
  93. package/docs/guides/many-to-many.md +0 -929
  94. package/docs/guides/subscriptions.md +0 -537
  95. package/docs/reference/api.md +0 -1373
  96. package/docs/reference/client.md +0 -224
  97. package/src/client/stores/index.js +0 -8
  98. package/src/client/stores/useAppStore.js +0 -285
  99. package/src/client/stores/useWsStore.js +0 -289
  100. package/src/client/ws.js +0 -762
  101. package/src/compiler/cli/compile-example.js +0 -33
  102. package/src/compiler/cli/compile-subscribable.js +0 -43
  103. package/src/compiler/cli/debug-compile.js +0 -44
  104. package/src/compiler/cli/debug-parse.js +0 -26
  105. package/src/compiler/cli/debug-path-parser.js +0 -18
  106. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  107. package/src/compiler/cli/index.js +0 -174
  108. package/src/compiler/codegen/auth-codegen.js +0 -153
  109. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  110. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  111. package/src/compiler/codegen/notification-codegen.js +0 -232
  112. package/src/compiler/codegen/operation-codegen.js +0 -1382
  113. package/src/compiler/codegen/permission-codegen.js +0 -318
  114. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  115. package/src/compiler/compiler.js +0 -371
  116. package/src/compiler/index.js +0 -11
  117. package/src/compiler/parser/entity-parser.js +0 -440
  118. package/src/compiler/parser/path-parser.js +0 -290
  119. package/src/compiler/parser/subscribable-parser.js +0 -244
  120. package/src/database/dzql-core.sql +0 -161
  121. package/src/database/migrations/001_schema.sql +0 -60
  122. package/src/database/migrations/002_functions.sql +0 -890
  123. package/src/database/migrations/003_operations.sql +0 -1135
  124. package/src/database/migrations/004_search.sql +0 -581
  125. package/src/database/migrations/005_entities.sql +0 -730
  126. package/src/database/migrations/006_auth.sql +0 -94
  127. package/src/database/migrations/007_events.sql +0 -133
  128. package/src/database/migrations/008_hello.sql +0 -18
  129. package/src/database/migrations/008a_meta.sql +0 -172
  130. package/src/database/migrations/009_subscriptions.sql +0 -240
  131. package/src/database/migrations/010_atomic_updates.sql +0 -157
  132. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  133. package/src/index.js +0 -40
  134. package/src/server/api.js +0 -9
  135. package/src/server/db.js +0 -442
  136. package/src/server/index.js +0 -317
  137. package/src/server/logger.js +0 -259
  138. package/src/server/mcp.js +0 -594
  139. package/src/server/meta-route.js +0 -251
  140. package/src/server/namespace.js +0 -292
  141. package/src/server/subscriptions.js +0 -351
  142. package/src/server/ws.js +0 -573
@@ -1,450 +0,0 @@
1
- /**
2
- * Graph Rules Code Generator
3
- * Generates PostgreSQL functions for graph rule execution
4
- */
5
-
6
- export class GraphRulesCodegen {
7
- constructor(tableName, graphRules, primaryKey = ['id']) {
8
- this.tableName = tableName;
9
- this.graphRules = graphRules;
10
- this.primaryKey = Array.isArray(primaryKey) ? primaryKey : [primaryKey];
11
- }
12
-
13
- /**
14
- * Generate all graph rule functions
15
- * @returns {string} SQL for graph rule functions
16
- */
17
- generate() {
18
- if (!this.graphRules || Object.keys(this.graphRules).length === 0) {
19
- return ''; // No functions if no rules
20
- }
21
-
22
- const functions = [];
23
-
24
- // Generate function for each trigger (on_create, on_update, on_delete)
25
- for (const [trigger, rules] of Object.entries(this.graphRules)) {
26
- const functionSQL = this._generateTriggerFunction(trigger, rules);
27
- if (functionSQL) {
28
- functions.push(functionSQL);
29
- }
30
- }
31
-
32
- return functions.join('\n\n');
33
- }
34
-
35
- /**
36
- * Generate function for a specific trigger
37
- * @private
38
- */
39
- _generateTriggerFunction(trigger, rules) {
40
- const operation = trigger.replace('on_', ''); // on_create -> create
41
- const functionName = `_graph_${this.tableName}_${trigger}`;
42
-
43
- const ruleBlocks = [];
44
-
45
- // Process each rule
46
- for (const [ruleName, ruleConfig] of Object.entries(rules)) {
47
- const description = ruleConfig.description || ruleName;
48
- const condition = ruleConfig.condition;
49
- const actions = Array.isArray(ruleConfig.actions)
50
- ? ruleConfig.actions
51
- : (ruleConfig.actions ? [ruleConfig.actions] : []);
52
-
53
- const actionBlocks = [];
54
- for (const action of actions) {
55
- const actionSQL = this._generateAction(action, ruleName, description, operation);
56
- if (actionSQL) {
57
- actionBlocks.push(actionSQL);
58
- }
59
- }
60
-
61
- if (actionBlocks.length === 0) {
62
- continue; // Skip rules with no actions
63
- }
64
-
65
- // Wrap actions in condition IF block if condition is present
66
- if (condition) {
67
- const conditionSQL = this._generateCondition(condition, operation);
68
- const ruleBlock = ` -- Rule: ${ruleName}
69
- IF ${conditionSQL} THEN
70
- ${actionBlocks.join('\n\n')}
71
- END IF;`;
72
- ruleBlocks.push(ruleBlock);
73
- } else {
74
- // No condition - add actions directly
75
- ruleBlocks.push(...actionBlocks);
76
- }
77
- }
78
-
79
- if (ruleBlocks.length === 0) {
80
- return null; // No actions, no function
81
- }
82
-
83
- // Determine parameters based on operation - p_user_id ALWAYS FIRST
84
- const params = operation === 'delete'
85
- ? `p_user_id INT,\n p_old_record JSONB`
86
- : operation === 'update'
87
- ? `p_user_id INT,\n p_old_record JSONB,\n p_new_record JSONB`
88
- : `p_user_id INT,\n p_record JSONB`;
89
-
90
- return `-- Graph rules: ${trigger} on ${this.tableName}
91
- CREATE OR REPLACE FUNCTION ${functionName}(
92
- ${params}
93
- ) RETURNS VOID AS $$
94
- BEGIN
95
- ${ruleBlocks.join('\n\n')}
96
- END;
97
- $$ LANGUAGE plpgsql SECURITY DEFINER;`;
98
- }
99
-
100
- /**
101
- * Generate SQL for a single action
102
- * @param {Object} action - The action configuration
103
- * @param {string} ruleName - The rule name
104
- * @param {string} description - The rule description
105
- * @param {string} operation - The operation context ('create', 'update', 'delete')
106
- * @private
107
- */
108
- _generateAction(action, ruleName, description, operation) {
109
- const comment = ` -- ${description}`;
110
-
111
- switch (action.type) {
112
- case 'create':
113
- return this._generateCreateAction(action, comment, operation);
114
-
115
- case 'update':
116
- return this._generateUpdateAction(action, comment, operation);
117
-
118
- case 'delete':
119
- return this._generateDeleteAction(action, comment, operation);
120
-
121
- case 'validate':
122
- return this._generateValidateAction(action, comment, operation);
123
-
124
- case 'execute':
125
- return this._generateExecuteAction(action, comment, operation);
126
-
127
- case 'notify':
128
- return this._generateNotifyAction(action, comment, operation);
129
-
130
- default:
131
- console.warn('Unknown action type:', action.type);
132
- return null;
133
- }
134
- }
135
-
136
- /**
137
- * Generate CREATE action
138
- * @private
139
- */
140
- _generateCreateAction(action, comment, operation) {
141
- const entity = action.entity;
142
- const data = action.data;
143
-
144
- const fields = [];
145
- const values = [];
146
-
147
- for (const [field, value] of Object.entries(data)) {
148
- fields.push(field);
149
- values.push(this._resolveValue(value, operation));
150
- }
151
-
152
- return `${comment}
153
- INSERT INTO ${entity} (${fields.join(', ')})
154
- VALUES (${values.join(', ')});`;
155
- }
156
-
157
- /**
158
- * Generate UPDATE action
159
- * @private
160
- */
161
- _generateUpdateAction(action, comment, operation) {
162
- const entity = action.entity;
163
- const data = action.data;
164
- const match = action.match;
165
-
166
- const setClauses = [];
167
- for (const [field, value] of Object.entries(data)) {
168
- setClauses.push(`${field} = ${this._resolveValue(value, operation)}`);
169
- }
170
-
171
- const whereClauses = [];
172
- for (const [field, value] of Object.entries(match)) {
173
- whereClauses.push(`${field} = ${this._resolveValue(value, operation)}`);
174
- }
175
-
176
- return `${comment}
177
- UPDATE ${entity}
178
- SET ${setClauses.join(', ')}
179
- WHERE ${whereClauses.join(' AND ')};`;
180
- }
181
-
182
- /**
183
- * Generate DELETE action
184
- * @private
185
- */
186
- _generateDeleteAction(action, comment, operation) {
187
- const entity = action.entity;
188
- const match = action.match;
189
-
190
- const whereClauses = [];
191
- for (const [field, value] of Object.entries(match)) {
192
- whereClauses.push(`${field} = ${this._resolveValue(value, operation)}`);
193
- }
194
-
195
- return `${comment}
196
- DELETE FROM ${entity}
197
- WHERE ${whereClauses.join(' AND ')};`;
198
- }
199
-
200
- /**
201
- * Generate VALIDATE action
202
- * @private
203
- */
204
- _generateValidateAction(action, comment, operation) {
205
- const functionName = action.function;
206
- const params = action.params || {};
207
- const errorMessage = action.error_message || 'Validation failed';
208
-
209
- const paramList = [];
210
- for (const [key, value] of Object.entries(params)) {
211
- paramList.push(`${key} => ${this._resolveValue(value, operation)}`);
212
- }
213
-
214
- const paramSQL = paramList.length > 0 ? paramList.join(', ') : '';
215
-
216
- return `${comment}
217
- IF NOT ${functionName}(${paramSQL}) THEN
218
- RAISE EXCEPTION '${errorMessage}';
219
- END IF;`;
220
- }
221
-
222
- /**
223
- * Generate EXECUTE action
224
- * @private
225
- */
226
- _generateExecuteAction(action, comment, operation) {
227
- const functionName = action.function;
228
- const params = action.params || {};
229
-
230
- const paramList = [];
231
- for (const [key, value] of Object.entries(params)) {
232
- paramList.push(`${key} => ${this._resolveValue(value, operation)}`);
233
- }
234
-
235
- const paramSQL = paramList.length > 0 ? paramList.join(', ') : '';
236
-
237
- return `${comment}
238
- PERFORM ${functionName}(${paramSQL});`;
239
- }
240
-
241
- /**
242
- * Generate jsonb_build_object for primary key columns from a JSONB record variable
243
- * Supports composite primary keys
244
- * @param {string} recordVar - The JSONB record variable name (e.g., 'p_record')
245
- * @private
246
- */
247
- _generatePKBuildObject(recordVar = 'p_record') {
248
- // Build jsonb_build_object with all primary key columns
249
- // e.g., jsonb_build_object('id', (p_record->>'id')::int)
250
- // or jsonb_build_object('template_id', (p_record->>'template_id')::int, 'depends_on_template_id', (p_record->>'depends_on_template_id')::int)
251
- const pairs = this.primaryKey.map(col => `'${col}', (${recordVar}->>'${col}')::int`);
252
- return `jsonb_build_object(${pairs.join(', ')})`;
253
- }
254
-
255
- /**
256
- * Generate NOTIFY action
257
- * Creates an event that will be broadcast to specified users
258
- * @private
259
- */
260
- _generateNotifyAction(action, comment, operation) {
261
- const users = action.users || [];
262
- const message = action.message || '';
263
- const data = action.data || {};
264
-
265
- // Determine the correct record variable based on operation
266
- const recordVar = operation === 'delete' ? 'p_old_record'
267
- : operation === 'update' ? 'p_new_record'
268
- : 'p_record';
269
-
270
- // Build user ID array resolution
271
- let userIdSQL = 'ARRAY[]::INT[]';
272
-
273
- if (users.length > 0) {
274
- // Users can be paths like "@post_id->posts.author_id" or direct field refs like "@author_id"
275
- const userPaths = [];
276
-
277
- for (const userPath of users) {
278
- if (userPath.startsWith('@') && !userPath.includes('->')) {
279
- // Simple field reference: @author_id
280
- const fieldName = userPath.substring(1);
281
- userPaths.push(`(${recordVar}->>'${fieldName}')::int`);
282
- } else if (userPath.startsWith('@') && userPath.includes('->')) {
283
- // Complex path: @post_id->posts.author_id - use runtime resolver
284
- userPaths.push(`dzql.resolve_notification_path('${this.tableName}', ${recordVar}, '${userPath}')`);
285
- } else {
286
- // Literal user ID
287
- userPaths.push(`${userPath}`);
288
- }
289
- }
290
-
291
- if (userPaths.length === 1 && !userPaths[0].includes('resolve_notification_path')) {
292
- // Single simple field - wrap in array
293
- userIdSQL = `ARRAY[${userPaths[0]}]`;
294
- } else if (userPaths.length === 1) {
295
- // Single path resolution (already returns array)
296
- userIdSQL = userPaths[0];
297
- } else {
298
- // Multiple paths - need to combine arrays
299
- userIdSQL = `(${userPaths.map(p =>
300
- p.includes('resolve_notification_path')
301
- ? p
302
- : `ARRAY[${p}]`
303
- ).join(' || ')})`;
304
- }
305
- }
306
-
307
- // Build notification data object
308
- const dataFields = [];
309
- dataFields.push(`'type', 'graph_rule_notification'`);
310
- dataFields.push(`'table', '${this.tableName}'`);
311
-
312
- if (message) {
313
- dataFields.push(`'message', ${this._resolveValue(message, operation)}`);
314
- }
315
-
316
- // Add custom data fields
317
- for (const [key, value] of Object.entries(data)) {
318
- dataFields.push(`'${key}', ${this._resolveValue(value, operation)}`);
319
- }
320
-
321
- const dataSQL = dataFields.length > 0
322
- ? `jsonb_build_object(${dataFields.join(', ')})`
323
- : "'{}'::jsonb";
324
-
325
- const pkBuildObject = this._generatePKBuildObject(recordVar);
326
-
327
- return `${comment}
328
- -- Create notification event
329
- INSERT INTO dzql.events (
330
- table_name,
331
- op,
332
- pk,
333
- data,
334
- user_id,
335
- notify_users
336
- ) VALUES (
337
- '${this.tableName}',
338
- 'notify',
339
- ${pkBuildObject},
340
- ${dataSQL},
341
- p_user_id,
342
- ${userIdSQL}
343
- );`;
344
- }
345
-
346
- /**
347
- * Generate condition SQL from condition string
348
- * Supports @before.field, @after.field, @user_id, @id
349
- * @private
350
- */
351
- _generateCondition(condition, operation) {
352
- let conditionSQL = condition;
353
-
354
- // Replace @before.field references (for update/delete)
355
- conditionSQL = conditionSQL.replace(/@before\.(\w+)/g, (match, field) => {
356
- return `(p_old_record->>'${field}')`;
357
- });
358
-
359
- // Replace @after.field references (for update)
360
- conditionSQL = conditionSQL.replace(/@after\.(\w+)/g, (match, field) => {
361
- if (operation === 'update') {
362
- return `(p_new_record->>'${field}')`;
363
- } else {
364
- return `(p_record->>'${field}')`;
365
- }
366
- });
367
-
368
- // Replace @field references (current record)
369
- conditionSQL = conditionSQL.replace(/@(\w+)(?!\w)/g, (match, field) => {
370
- if (field === 'user_id') {
371
- return 'p_user_id';
372
- } else if (field === 'id') {
373
- // Use appropriate record based on operation
374
- if (operation === 'update') {
375
- return `(p_new_record->>'id')`;
376
- } else if (operation === 'delete') {
377
- return `(p_old_record->>'id')`;
378
- } else {
379
- return `(p_record->>'id')`;
380
- }
381
- } else {
382
- // Field from current record
383
- if (operation === 'update') {
384
- return `(p_new_record->>'${field}')`;
385
- } else if (operation === 'delete') {
386
- return `(p_old_record->>'${field}')`;
387
- } else {
388
- return `(p_record->>'${field}')`;
389
- }
390
- }
391
- });
392
-
393
- return conditionSQL;
394
- }
395
-
396
- /**
397
- * Resolve a value (variable reference or literal)
398
- * @param {string|number} value - The value to resolve
399
- * @param {string} operation - The operation context ('create', 'update', 'delete')
400
- * @private
401
- */
402
- _resolveValue(value, operation = 'create') {
403
- if (typeof value !== 'string') {
404
- // Number or other type
405
- return value;
406
- }
407
-
408
- // Handle special variables
409
- if (value.startsWith('@')) {
410
- const varName = value.substring(1);
411
-
412
- // Special keywords
413
- switch (varName) {
414
- case 'user_id':
415
- return 'p_user_id';
416
-
417
- case 'today':
418
- return 'CURRENT_DATE';
419
-
420
- case 'now':
421
- return 'NOW()';
422
-
423
- default:
424
- // Field reference from record - use correct variable based on operation
425
- if (operation === 'delete') {
426
- return `(p_old_record->>'${varName}')`;
427
- } else if (operation === 'update') {
428
- return `(p_new_record->>'${varName}')`;
429
- } else {
430
- return `(p_record->>'${varName}')`;
431
- }
432
- }
433
- }
434
-
435
- // String literal
436
- return `'${value}'`;
437
- }
438
- }
439
-
440
- /**
441
- * Generate graph rule functions for an entity
442
- * @param {string} tableName - Table name
443
- * @param {Object} graphRules - Graph rules object
444
- * @param {Array<string>} primaryKey - Primary key column(s) (defaults to ['id'])
445
- * @returns {string} SQL for graph rule functions
446
- */
447
- export function generateGraphRuleFunctions(tableName, graphRules, primaryKey = ['id']) {
448
- const codegen = new GraphRulesCodegen(tableName, graphRules, primaryKey);
449
- return codegen.generate();
450
- }
@@ -1,232 +0,0 @@
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
- }