dzql 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/package.json +4 -3
- package/src/compiler/cli/index.js +174 -0
- package/src/compiler/codegen/graph-rules-codegen.js +259 -0
- package/src/compiler/codegen/notification-codegen.js +232 -0
- package/src/compiler/codegen/operation-codegen.js +555 -0
- package/src/compiler/codegen/permission-codegen.js +310 -0
- package/src/compiler/compiler.js +228 -0
- package/src/compiler/index.js +11 -0
- package/src/compiler/parser/entity-parser.js +299 -0
- package/src/compiler/parser/path-parser.js +290 -0
- package/src/database/migrations/002_functions.sql +39 -2
- package/src/database/migrations/003_operations.sql +10 -0
- package/src/database/migrations/005_entities.sql +112 -0
- package/GETTING_STARTED.md +0 -1104
- package/REFERENCE.md +0 -960
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity Definition Parser
|
|
3
|
+
* Parses entity registration calls and extracts configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class EntityParser {
|
|
7
|
+
/**
|
|
8
|
+
* Parse a dzql.register_entity() call from SQL
|
|
9
|
+
* @param {string} sql - SQL containing register_entity call
|
|
10
|
+
* @returns {Object} Parsed entity configuration
|
|
11
|
+
*/
|
|
12
|
+
parseFromSQL(sql) {
|
|
13
|
+
// Extract the register_entity call
|
|
14
|
+
const registerMatch = sql.match(/dzql\.register_entity\s*\(([\s\S]*?)\);/i);
|
|
15
|
+
if (!registerMatch) {
|
|
16
|
+
throw new Error('No register_entity call found in SQL');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const params = this._parseParameters(registerMatch[1]);
|
|
20
|
+
|
|
21
|
+
return this._buildEntityConfig(params);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse parameters from register_entity call
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
_parseParameters(paramsString) {
|
|
29
|
+
// Split by commas that are not inside quotes, parentheses, or brackets
|
|
30
|
+
const params = [];
|
|
31
|
+
let currentParam = '';
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let inString = false;
|
|
34
|
+
let stringChar = null;
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < paramsString.length; i++) {
|
|
37
|
+
const char = paramsString[i];
|
|
38
|
+
const prevChar = i > 0 ? paramsString[i - 1] : '';
|
|
39
|
+
|
|
40
|
+
if ((char === "'" || char === '"') && prevChar !== '\\') {
|
|
41
|
+
if (!inString) {
|
|
42
|
+
inString = true;
|
|
43
|
+
stringChar = char;
|
|
44
|
+
} else if (char === stringChar) {
|
|
45
|
+
inString = false;
|
|
46
|
+
stringChar = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!inString) {
|
|
51
|
+
if (char === '(' || char === '{' || char === '[') depth++;
|
|
52
|
+
if (char === ')' || char === '}' || char === ']') depth--;
|
|
53
|
+
|
|
54
|
+
if (char === ',' && depth === 0) {
|
|
55
|
+
params.push(currentParam.trim());
|
|
56
|
+
currentParam = '';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
currentParam += char;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (currentParam.trim()) {
|
|
65
|
+
params.push(currentParam.trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return params;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build entity configuration from parsed parameters
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
_buildEntityConfig(params) {
|
|
76
|
+
const config = {
|
|
77
|
+
tableName: this._cleanString(params[0]),
|
|
78
|
+
labelField: this._cleanString(params[1]),
|
|
79
|
+
searchableFields: this._parseArray(params[2]),
|
|
80
|
+
fkIncludes: params[3] ? this._parseJSON(params[3]) : {},
|
|
81
|
+
softDelete: params[4] ? this._parseBoolean(params[4]) : false,
|
|
82
|
+
temporalFields: params[5] ? this._parseJSON(params[5]) : {},
|
|
83
|
+
notificationPaths: params[6] ? this._parseJSON(params[6]) : {},
|
|
84
|
+
permissionPaths: params[7] ? this._parseJSON(params[7]) : {},
|
|
85
|
+
graphRules: params[8] ? this._parseJSON(params[8]) : {}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clean a string parameter (remove quotes)
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_cleanString(str) {
|
|
96
|
+
if (!str) return '';
|
|
97
|
+
// Remove outer quotes, SQL comments, then any remaining quotes and whitespace
|
|
98
|
+
let cleaned = str.replace(/^['"]|['"]$/g, ''); // Remove outer quotes
|
|
99
|
+
cleaned = cleaned.replace(/--[^\n]*/g, ''); // Remove SQL comments
|
|
100
|
+
cleaned = cleaned.replace(/['"\s]+$/g, ''); // Remove trailing quotes/whitespace
|
|
101
|
+
return cleaned.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse an array parameter
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_parseArray(str) {
|
|
109
|
+
if (!str || str === 'array[]::text[]') return [];
|
|
110
|
+
|
|
111
|
+
// Handle array['item1', 'item2'] format
|
|
112
|
+
// Use greedy match to get everything up to the last ] before optional ::type
|
|
113
|
+
const match = str.match(/array\[(.*)\](?:::.*)?$/i);
|
|
114
|
+
if (!match) return [];
|
|
115
|
+
|
|
116
|
+
// Split on commas that are not inside brackets or quotes
|
|
117
|
+
const items = [];
|
|
118
|
+
let current = '';
|
|
119
|
+
let depth = 0;
|
|
120
|
+
let inString = false;
|
|
121
|
+
let stringChar = null;
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < match[1].length; i++) {
|
|
124
|
+
const char = match[1][i];
|
|
125
|
+
const prev = i > 0 ? match[1][i - 1] : '';
|
|
126
|
+
|
|
127
|
+
if ((char === "'" || char === '"') && prev !== '\\') {
|
|
128
|
+
if (!inString) {
|
|
129
|
+
inString = true;
|
|
130
|
+
stringChar = char;
|
|
131
|
+
} else if (char === stringChar) {
|
|
132
|
+
inString = false;
|
|
133
|
+
stringChar = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!inString) {
|
|
138
|
+
if (char === '[') depth++;
|
|
139
|
+
if (char === ']') depth--;
|
|
140
|
+
|
|
141
|
+
if (char === ',' && depth === 0) {
|
|
142
|
+
items.push(current.trim().replace(/^['"]|['"]$/g, ''));
|
|
143
|
+
current = '';
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
current += char;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (current.trim()) {
|
|
152
|
+
items.push(current.trim().replace(/^['"]|['"]$/g, ''));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return items.filter(item => item.length > 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse a JSONB parameter
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
_parseJSON(str) {
|
|
163
|
+
if (!str || str === '{}' || str === "'{}'") return {};
|
|
164
|
+
|
|
165
|
+
// Handle jsonb_build_object(...) calls
|
|
166
|
+
if (str.includes('jsonb_build_object')) {
|
|
167
|
+
return this._parseJSONBuildObject(str);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle JSON string literals
|
|
171
|
+
if (str.startsWith("'") && str.endsWith("'")) {
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(str.slice(1, -1).replace(/''/g, "'"));
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.warn('Failed to parse JSON:', str, e);
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parse jsonb_build_object(...) calls recursively
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
_parseJSONBuildObject(str) {
|
|
188
|
+
// Extract the content between jsonb_build_object( and )
|
|
189
|
+
const match = str.match(/jsonb_build_object\s*\(([\s\S]*)\)/i);
|
|
190
|
+
if (!match) return {};
|
|
191
|
+
|
|
192
|
+
const content = match[1];
|
|
193
|
+
const params = this._parseParameters(content);
|
|
194
|
+
const result = {};
|
|
195
|
+
|
|
196
|
+
// Process key-value pairs
|
|
197
|
+
for (let i = 0; i < params.length; i += 2) {
|
|
198
|
+
if (i + 1 >= params.length) break;
|
|
199
|
+
|
|
200
|
+
const key = this._cleanString(params[i]);
|
|
201
|
+
const value = params[i + 1];
|
|
202
|
+
|
|
203
|
+
// Handle nested jsonb_build_object
|
|
204
|
+
if (value.includes('jsonb_build_object')) {
|
|
205
|
+
result[key] = this._parseJSONBuildObject(value);
|
|
206
|
+
}
|
|
207
|
+
// Handle jsonb_build_array
|
|
208
|
+
else if (value.includes('jsonb_build_array')) {
|
|
209
|
+
result[key] = this._parseJSONBuildArray(value);
|
|
210
|
+
}
|
|
211
|
+
// Handle array literal
|
|
212
|
+
else if (value.startsWith('array[')) {
|
|
213
|
+
result[key] = this._parseArray(value);
|
|
214
|
+
}
|
|
215
|
+
// Handle simple values
|
|
216
|
+
else {
|
|
217
|
+
const cleanValue = this._cleanString(value);
|
|
218
|
+
result[key] = cleanValue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse jsonb_build_array(...) calls
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
_parseJSONBuildArray(str) {
|
|
230
|
+
const match = str.match(/jsonb_build_array\s*\(([\s\S]*)\)/i);
|
|
231
|
+
if (!match) return [];
|
|
232
|
+
|
|
233
|
+
const content = match[1];
|
|
234
|
+
const params = this._parseParameters(content);
|
|
235
|
+
|
|
236
|
+
return params.map(param => {
|
|
237
|
+
if (param.includes('jsonb_build_object')) {
|
|
238
|
+
return this._parseJSONBuildObject(param);
|
|
239
|
+
}
|
|
240
|
+
return this._cleanString(param);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse boolean value
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_parseBoolean(str) {
|
|
249
|
+
const cleaned = str.trim().toLowerCase();
|
|
250
|
+
return cleaned === 'true' || cleaned === 't';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse entity definition from JS object (for programmatic use)
|
|
255
|
+
* @param {Object} entity - Entity definition object
|
|
256
|
+
* @returns {Object} Normalized entity configuration
|
|
257
|
+
*/
|
|
258
|
+
parseFromObject(entity) {
|
|
259
|
+
return {
|
|
260
|
+
tableName: entity.tableName || entity.table,
|
|
261
|
+
labelField: entity.labelField || 'name',
|
|
262
|
+
searchableFields: entity.searchableFields || [],
|
|
263
|
+
fkIncludes: entity.fkIncludes || {},
|
|
264
|
+
softDelete: entity.softDelete || false,
|
|
265
|
+
temporalFields: entity.temporalFields || {},
|
|
266
|
+
notificationPaths: entity.notificationPaths || {},
|
|
267
|
+
permissionPaths: entity.permissionPaths || {},
|
|
268
|
+
graphRules: entity.graphRules || {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Parse all entities from a SQL file
|
|
275
|
+
* @param {string} sql - SQL file content
|
|
276
|
+
* @returns {Array} Array of parsed entity configurations
|
|
277
|
+
*/
|
|
278
|
+
export function parseEntitiesFromSQL(sql) {
|
|
279
|
+
const parser = new EntityParser();
|
|
280
|
+
const entities = [];
|
|
281
|
+
|
|
282
|
+
// Find all register_entity calls
|
|
283
|
+
const registerCalls = sql.match(/dzql\.register_entity\s*\([\s\S]*?\);/gi);
|
|
284
|
+
|
|
285
|
+
if (!registerCalls) {
|
|
286
|
+
return entities;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const call of registerCalls) {
|
|
290
|
+
try {
|
|
291
|
+
const entity = parser.parseFromSQL(call);
|
|
292
|
+
entities.push(entity);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.warn('Failed to parse entity:', error.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return entities;
|
|
299
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission/Notification Path Parser
|
|
3
|
+
* Parses DZQL path DSL into AST for code generation
|
|
4
|
+
*
|
|
5
|
+
* Path Grammar:
|
|
6
|
+
* - Direct field: @field_name
|
|
7
|
+
* - FK traversal: @field->table.target_field
|
|
8
|
+
* - Conditional: @field->table[condition]{temporal}.target_field
|
|
9
|
+
* - Complex: field1.field2->table[filter].target
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class PathParser {
|
|
13
|
+
/**
|
|
14
|
+
* Parse a permission/notification path into an AST
|
|
15
|
+
* @param {string} path - Path string to parse
|
|
16
|
+
* @returns {Object} AST representation
|
|
17
|
+
*/
|
|
18
|
+
parse(path) {
|
|
19
|
+
if (!path || path.trim() === '') {
|
|
20
|
+
return { type: 'empty' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Direct field reference: @owner_id
|
|
24
|
+
if (path.match(/^@\w+$/)) {
|
|
25
|
+
return {
|
|
26
|
+
type: 'direct_field',
|
|
27
|
+
field: path.substring(1)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Complex path with traversal
|
|
32
|
+
if (path.includes('->')) {
|
|
33
|
+
return this._parseTraversalPath(path);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Field path with dot notation: field1.field2
|
|
37
|
+
if (path.includes('.') && !path.includes('->')) {
|
|
38
|
+
return this._parseDotPath(path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Unknown format
|
|
42
|
+
return {
|
|
43
|
+
type: 'unknown',
|
|
44
|
+
path
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a traversal path (contains ->)
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
_parseTraversalPath(path) {
|
|
53
|
+
const steps = [];
|
|
54
|
+
const parts = path.split('->');
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < parts.length; i++) {
|
|
57
|
+
const part = parts[i].trim();
|
|
58
|
+
|
|
59
|
+
if (i === 0) {
|
|
60
|
+
// First part: source field(s)
|
|
61
|
+
steps.push(this._parseSourceField(part));
|
|
62
|
+
} else if (i === parts.length - 1) {
|
|
63
|
+
// Last part: target field
|
|
64
|
+
steps.push(this._parseTargetField(part));
|
|
65
|
+
} else {
|
|
66
|
+
// Middle part: table with optional condition
|
|
67
|
+
steps.push(this._parseTableReference(part));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
type: 'traversal',
|
|
73
|
+
steps
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse source field (can be @field or field.subfield)
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
_parseSourceField(part) {
|
|
82
|
+
if (part.startsWith('@')) {
|
|
83
|
+
return {
|
|
84
|
+
type: 'field_ref',
|
|
85
|
+
field: part.substring(1)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (part.includes('.')) {
|
|
90
|
+
const fields = part.split('.');
|
|
91
|
+
return {
|
|
92
|
+
type: 'dot_path',
|
|
93
|
+
fields
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
type: 'field_ref',
|
|
99
|
+
field: part
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse table reference with optional filter and temporal marker
|
|
105
|
+
* Example: acts_for[org_id=$,role='admin']{active}
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_parseTableReference(part) {
|
|
109
|
+
const result = {
|
|
110
|
+
type: 'table_ref',
|
|
111
|
+
table: null,
|
|
112
|
+
filter: null,
|
|
113
|
+
temporal: false,
|
|
114
|
+
targetField: null
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Extract temporal marker {active}
|
|
118
|
+
if (part.includes('{active}')) {
|
|
119
|
+
result.temporal = true;
|
|
120
|
+
part = part.replace('{active}', '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract filter [condition]
|
|
124
|
+
const filterMatch = part.match(/([a-z_]+)\[(.*?)\](\.(.+))?/i);
|
|
125
|
+
if (filterMatch) {
|
|
126
|
+
result.table = filterMatch[1];
|
|
127
|
+
result.filter = this._parseFilter(filterMatch[2]);
|
|
128
|
+
result.targetField = filterMatch[4] || null;
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Simple table.field reference
|
|
133
|
+
const dotMatch = part.match(/([a-z_]+)\.(.+)/i);
|
|
134
|
+
if (dotMatch) {
|
|
135
|
+
result.table = dotMatch[1];
|
|
136
|
+
result.targetField = dotMatch[2];
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Just table name
|
|
141
|
+
result.table = part;
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse filter conditions
|
|
147
|
+
* Example: org_id=$,role='admin'
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
_parseFilter(filterStr) {
|
|
151
|
+
const conditions = [];
|
|
152
|
+
const parts = filterStr.split(',');
|
|
153
|
+
|
|
154
|
+
for (const part of parts) {
|
|
155
|
+
const trimmed = part.trim();
|
|
156
|
+
|
|
157
|
+
// Handle various comparison operators
|
|
158
|
+
if (trimmed.includes('=')) {
|
|
159
|
+
const [field, value] = trimmed.split('=').map(s => s.trim());
|
|
160
|
+
conditions.push({
|
|
161
|
+
field,
|
|
162
|
+
operator: '=',
|
|
163
|
+
value: value === '$' ? { type: 'param' } : this._parseValue(value)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return conditions;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse a value (string literal, number, param reference)
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
_parseValue(value) {
|
|
176
|
+
// String literal
|
|
177
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
178
|
+
return {
|
|
179
|
+
type: 'literal',
|
|
180
|
+
value: value.slice(1, -1)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Number
|
|
185
|
+
if (/^\d+$/.test(value)) {
|
|
186
|
+
return {
|
|
187
|
+
type: 'number',
|
|
188
|
+
value: parseInt(value)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Field reference
|
|
193
|
+
if (value.startsWith('@')) {
|
|
194
|
+
return {
|
|
195
|
+
type: 'field',
|
|
196
|
+
value: value.substring(1)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
type: 'literal',
|
|
202
|
+
value
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Parse target field (last part of path)
|
|
208
|
+
* Example: user_id or users.user_id
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
_parseTargetField(part) {
|
|
212
|
+
// Check for temporal marker before removing it
|
|
213
|
+
const hasTemporal = part.includes('{active}');
|
|
214
|
+
|
|
215
|
+
// Remove temporal marker if present
|
|
216
|
+
part = part.replace('{active}', '');
|
|
217
|
+
|
|
218
|
+
// Check for table[filter].field pattern
|
|
219
|
+
const filterMatch = part.match(/([a-z_]+)\[(.*?)\]\.(.+)/i);
|
|
220
|
+
if (filterMatch) {
|
|
221
|
+
return {
|
|
222
|
+
type: 'table_ref',
|
|
223
|
+
table: filterMatch[1],
|
|
224
|
+
filter: this._parseFilter(filterMatch[2]),
|
|
225
|
+
temporal: hasTemporal,
|
|
226
|
+
targetField: filterMatch[3]
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Simple field reference
|
|
231
|
+
if (!part.includes('.')) {
|
|
232
|
+
return {
|
|
233
|
+
type: 'field_ref',
|
|
234
|
+
field: part
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Dot notation
|
|
239
|
+
const fields = part.split('.');
|
|
240
|
+
return {
|
|
241
|
+
type: 'dot_path',
|
|
242
|
+
fields
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Parse dot path (field.subfield.subsubfield)
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
_parseDotPath(path) {
|
|
251
|
+
const fields = path.split('.').map(f => f.trim());
|
|
252
|
+
return {
|
|
253
|
+
type: 'dot_path',
|
|
254
|
+
fields
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Parse multiple paths (used in permission arrays)
|
|
260
|
+
* @param {Array<string>} paths - Array of path strings
|
|
261
|
+
* @returns {Array<Object>} Array of ASTs
|
|
262
|
+
*/
|
|
263
|
+
parseMultiple(paths) {
|
|
264
|
+
if (!Array.isArray(paths)) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return paths.map(path => this.parse(path));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Utility function to parse a single path
|
|
274
|
+
* @param {string} path - Path to parse
|
|
275
|
+
* @returns {Object} AST
|
|
276
|
+
*/
|
|
277
|
+
export function parsePath(path) {
|
|
278
|
+
const parser = new PathParser();
|
|
279
|
+
return parser.parse(path);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Utility function to parse multiple paths
|
|
284
|
+
* @param {Array<string>} paths - Paths to parse
|
|
285
|
+
* @returns {Array<Object>} ASTs
|
|
286
|
+
*/
|
|
287
|
+
export function parsePaths(paths) {
|
|
288
|
+
const parser = new PathParser();
|
|
289
|
+
return parser.parseMultiple(paths);
|
|
290
|
+
}
|
|
@@ -476,9 +476,26 @@ BEGIN
|
|
|
476
476
|
l_temp_value int;
|
|
477
477
|
l_segment_table text;
|
|
478
478
|
l_segment_record jsonb;
|
|
479
|
+
l_replacement_value text;
|
|
479
480
|
BEGIN
|
|
480
481
|
FOR l_j IN 0..jsonb_array_length(l_current_result) - 1 LOOP
|
|
481
|
-
|
|
482
|
+
l_replacement_value := (l_current_result->>l_j)::text;
|
|
483
|
+
|
|
484
|
+
-- Check if $ appears in a condition context table[field=$]
|
|
485
|
+
-- If so, we need to quote it properly for text values
|
|
486
|
+
IF l_continuation_parts[l_i] ~ '\[.*\$.*\]' THEN
|
|
487
|
+
-- Try to parse as integer first
|
|
488
|
+
BEGIN
|
|
489
|
+
-- If it's an integer, use it directly
|
|
490
|
+
l_temp_segment := replace(l_continuation_parts[l_i], '$', l_replacement_value::int::text);
|
|
491
|
+
EXCEPTION WHEN OTHERS THEN
|
|
492
|
+
-- Not an integer, quote it as a literal
|
|
493
|
+
l_temp_segment := replace(l_continuation_parts[l_i], '$', quote_literal(l_replacement_value));
|
|
494
|
+
END;
|
|
495
|
+
ELSE
|
|
496
|
+
-- Not in a condition, use raw value
|
|
497
|
+
l_temp_segment := replace(l_continuation_parts[l_i], '$', l_replacement_value);
|
|
498
|
+
END IF;
|
|
482
499
|
|
|
483
500
|
-- Handle table.field syntax in continuation segments
|
|
484
501
|
IF l_temp_segment ~ '^[a-z_]+\.[a-z_]+' THEN
|
|
@@ -498,7 +515,27 @@ BEGIN
|
|
|
498
515
|
l_current_result := to_jsonb(l_current_ids);
|
|
499
516
|
ELSE
|
|
500
517
|
-- Single value result
|
|
501
|
-
|
|
518
|
+
DECLARE
|
|
519
|
+
l_replacement_value text;
|
|
520
|
+
BEGIN
|
|
521
|
+
l_replacement_value := l_current_result::text;
|
|
522
|
+
|
|
523
|
+
-- Check if $ appears in a condition context table[field=$]
|
|
524
|
+
-- If so, we need to quote it properly for text values
|
|
525
|
+
IF l_current_segment ~ '\[.*\$.*\]' THEN
|
|
526
|
+
-- Try to parse as integer first
|
|
527
|
+
BEGIN
|
|
528
|
+
-- If it's an integer, use it directly
|
|
529
|
+
l_current_segment := replace(l_current_segment, '$', l_replacement_value::int::text);
|
|
530
|
+
EXCEPTION WHEN OTHERS THEN
|
|
531
|
+
-- Not an integer, quote it as a literal
|
|
532
|
+
l_current_segment := replace(l_current_segment, '$', quote_literal(l_replacement_value));
|
|
533
|
+
END;
|
|
534
|
+
ELSE
|
|
535
|
+
-- Not in a condition, use raw value
|
|
536
|
+
l_current_segment := replace(l_current_segment, '$', l_replacement_value);
|
|
537
|
+
END IF;
|
|
538
|
+
END;
|
|
502
539
|
|
|
503
540
|
-- Handle table.field syntax in continuation segments
|
|
504
541
|
IF l_current_segment ~ '^[a-z_]+\.[a-z_]+' THEN
|
|
@@ -349,6 +349,16 @@ BEGIN
|
|
|
349
349
|
ELSE
|
|
350
350
|
-- INSERT: Use provided values, let database handle defaults
|
|
351
351
|
|
|
352
|
+
-- Auto-inject user_id if table has a user_id column and it's not provided
|
|
353
|
+
IF EXISTS (
|
|
354
|
+
SELECT 1 FROM information_schema.columns
|
|
355
|
+
WHERE table_schema = 'public'
|
|
356
|
+
AND table_name = p_entity
|
|
357
|
+
AND column_name = 'user_id'
|
|
358
|
+
) AND (l_args_json->>'user_id' IS NULL) THEN
|
|
359
|
+
l_args_json := l_args_json || jsonb_build_object('user_id', p_user_id);
|
|
360
|
+
END IF;
|
|
361
|
+
|
|
352
362
|
-- Check create permission on new values
|
|
353
363
|
l_operation := 'create';
|
|
354
364
|
l_permission_record := l_args_json;
|