dzql 0.5.32 → 0.6.0

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 (150) 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 +293 -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 +641 -0
  20. package/docs/project-setup.md +432 -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 +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. 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
- }