dzql 0.5.33 → 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.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- 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
|
-
}
|