@rapidd/build 1.0.8 → 1.1.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.
- package/README.md +13 -22
- package/package.json +1 -1
- package/src/commands/build.js +6 -1
- package/src/generators/aclGenerator.js +0 -1
- package/src/generators/relationshipsGenerator.js +25 -26
- package/src/generators/routeGenerator.js +1 -1
- package/src/parsers/prismaParser.js +11 -1
- package/src/parsers/advancedRLSConverter.js +0 -305
- package/src/parsers/autoRLSConverter.js +0 -322
- package/src/parsers/dynamicRLSConverter.js +0 -379
- package/src/parsers/postgresRLSConverter.js +0 -192
- package/src/parsers/sqlToJsConverter.js +0 -611
|
@@ -1,611 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQL to JavaScript/Prisma Filter Converter
|
|
3
|
-
* Converts PostgreSQL RLS policy expressions to JavaScript and Prisma filters
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Parse SQL expression into an AST (Abstract Syntax Tree)
|
|
8
|
-
* @param {string} sql - SQL expression
|
|
9
|
-
* @returns {Object} - Parsed AST
|
|
10
|
-
*/
|
|
11
|
-
function parseSqlExpression(sql) {
|
|
12
|
-
if (!sql) return null;
|
|
13
|
-
|
|
14
|
-
// Clean up the SQL
|
|
15
|
-
sql = sql.trim();
|
|
16
|
-
|
|
17
|
-
// Handle parentheses and logical operators
|
|
18
|
-
return parseLogicalExpression(sql);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parse logical expressions (AND, OR)
|
|
23
|
-
* @param {string} sql - SQL expression
|
|
24
|
-
* @returns {Object} - AST node
|
|
25
|
-
*/
|
|
26
|
-
function parseLogicalExpression(sql) {
|
|
27
|
-
// Find top-level OR (lowest precedence)
|
|
28
|
-
const orParts = splitByOperator(sql, 'OR');
|
|
29
|
-
if (orParts.length > 1) {
|
|
30
|
-
return {
|
|
31
|
-
type: 'OR',
|
|
32
|
-
operands: orParts.map(part => parseLogicalExpression(part))
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Find top-level AND (higher precedence than OR)
|
|
37
|
-
const andParts = splitByOperator(sql, 'AND');
|
|
38
|
-
if (andParts.length > 1) {
|
|
39
|
-
return {
|
|
40
|
-
type: 'AND',
|
|
41
|
-
operands: andParts.map(part => parseLogicalExpression(part))
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Parse comparison or other expressions
|
|
46
|
-
return parseComparisonExpression(sql);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Split SQL by operator at the same nesting level
|
|
51
|
-
* @param {string} sql - SQL expression
|
|
52
|
-
* @param {string} operator - Operator to split by (AND, OR)
|
|
53
|
-
* @returns {Array} - Parts split by operator
|
|
54
|
-
*/
|
|
55
|
-
function splitByOperator(sql, operator) {
|
|
56
|
-
const parts = [];
|
|
57
|
-
let current = '';
|
|
58
|
-
let depth = 0;
|
|
59
|
-
let i = 0;
|
|
60
|
-
|
|
61
|
-
while (i < sql.length) {
|
|
62
|
-
const char = sql[i];
|
|
63
|
-
|
|
64
|
-
if (char === '(') {
|
|
65
|
-
depth++;
|
|
66
|
-
current += char;
|
|
67
|
-
i++;
|
|
68
|
-
} else if (char === ')') {
|
|
69
|
-
depth--;
|
|
70
|
-
current += char;
|
|
71
|
-
i++;
|
|
72
|
-
} else if (depth === 0) {
|
|
73
|
-
// Check if we're at the operator
|
|
74
|
-
const remaining = sql.substring(i);
|
|
75
|
-
const operatorPattern = new RegExp(`^\\s+${operator}\\s+`, 'i');
|
|
76
|
-
const match = remaining.match(operatorPattern);
|
|
77
|
-
|
|
78
|
-
if (match) {
|
|
79
|
-
parts.push(current.trim());
|
|
80
|
-
current = '';
|
|
81
|
-
i += match[0].length;
|
|
82
|
-
} else {
|
|
83
|
-
current += char;
|
|
84
|
-
i++;
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
current += char;
|
|
88
|
-
i++;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (current.trim()) {
|
|
93
|
-
parts.push(current.trim());
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return parts.length > 0 ? parts : [sql];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Parse comparison expressions
|
|
101
|
-
* @param {string} sql - SQL expression
|
|
102
|
-
* @returns {Object} - AST node
|
|
103
|
-
*/
|
|
104
|
-
function parseComparisonExpression(sql) {
|
|
105
|
-
sql = sql.trim();
|
|
106
|
-
|
|
107
|
-
// Remove outer parentheses
|
|
108
|
-
if (sql.startsWith('(') && sql.endsWith(')')) {
|
|
109
|
-
const inner = sql.substring(1, sql.length - 1);
|
|
110
|
-
// Make sure these are matching parentheses
|
|
111
|
-
if (countParentheses(inner) === 0) {
|
|
112
|
-
return parseLogicalExpression(inner);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Handle NOT
|
|
117
|
-
if (sql.toUpperCase().startsWith('NOT ')) {
|
|
118
|
-
return {
|
|
119
|
-
type: 'NOT',
|
|
120
|
-
operand: parseLogicalExpression(sql.substring(4).trim())
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Handle = ANY (ARRAY[...]) pattern (PostgreSQL specific)
|
|
125
|
-
const anyArrayMatch = sql.match(/^(.+?)\s*=\s*ANY\s*\(\s*ARRAY\s*\[([^\]]+)\]\s*\)/i);
|
|
126
|
-
if (anyArrayMatch) {
|
|
127
|
-
const left = anyArrayMatch[1].trim();
|
|
128
|
-
const arrayItems = anyArrayMatch[2].split(',').map(item => item.trim());
|
|
129
|
-
return {
|
|
130
|
-
type: 'COMPARISON',
|
|
131
|
-
operator: 'IN',
|
|
132
|
-
left: parseValue(left),
|
|
133
|
-
right: {
|
|
134
|
-
type: 'ARRAY',
|
|
135
|
-
items: arrayItems.map(item => parseValue(item))
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Handle IN (SELECT...) - mark as subquery
|
|
141
|
-
const inSelectMatch = sql.match(/^(.+?)\s+IN\s+\(SELECT/i);
|
|
142
|
-
if (inSelectMatch) {
|
|
143
|
-
return {
|
|
144
|
-
type: 'COMPARISON',
|
|
145
|
-
operator: 'IN',
|
|
146
|
-
left: parseValue(inSelectMatch[1].trim()),
|
|
147
|
-
right: { type: 'SUBQUERY', value: '/* Subquery - needs manual implementation */' }
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Parse comparison operators
|
|
152
|
-
const comparisonOps = ['<=', '>=', '<>', '!=', '=', '<', '>', ' IN ', ' NOT IN ', ' LIKE ', ' ILIKE ', ' IS NULL', ' IS NOT NULL'];
|
|
153
|
-
|
|
154
|
-
for (const op of comparisonOps) {
|
|
155
|
-
const index = findOperatorIndex(sql, op);
|
|
156
|
-
if (index !== -1) {
|
|
157
|
-
const left = sql.substring(0, index).trim();
|
|
158
|
-
const right = sql.substring(index + op.length).trim();
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
type: 'COMPARISON',
|
|
162
|
-
operator: op.trim(),
|
|
163
|
-
left: parseValue(left),
|
|
164
|
-
right: op.trim().includes('IS') ? null : parseValue(right)
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// If no operator found, treat as a value
|
|
170
|
-
return parseValue(sql);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Find operator index outside of parentheses and quotes
|
|
175
|
-
* @param {string} sql - SQL expression
|
|
176
|
-
* @param {string} operator - Operator to find
|
|
177
|
-
* @returns {number} - Index or -1
|
|
178
|
-
*/
|
|
179
|
-
function findOperatorIndex(sql, operator) {
|
|
180
|
-
const upperSql = sql.toUpperCase();
|
|
181
|
-
const upperOp = operator.toUpperCase();
|
|
182
|
-
let depth = 0;
|
|
183
|
-
let inQuotes = false;
|
|
184
|
-
let quoteChar = '';
|
|
185
|
-
|
|
186
|
-
for (let i = 0; i < sql.length; i++) {
|
|
187
|
-
const char = sql[i];
|
|
188
|
-
|
|
189
|
-
if ((char === "'" || char === '"') && !inQuotes) {
|
|
190
|
-
inQuotes = true;
|
|
191
|
-
quoteChar = char;
|
|
192
|
-
} else if (char === quoteChar && inQuotes) {
|
|
193
|
-
inQuotes = false;
|
|
194
|
-
quoteChar = '';
|
|
195
|
-
} else if (!inQuotes) {
|
|
196
|
-
if (char === '(') depth++;
|
|
197
|
-
else if (char === ')') depth--;
|
|
198
|
-
else if (depth === 0 && upperSql.substring(i, i + upperOp.length) === upperOp) {
|
|
199
|
-
// Check word boundaries for operators like IN
|
|
200
|
-
if (upperOp.startsWith(' ')) {
|
|
201
|
-
return i;
|
|
202
|
-
}
|
|
203
|
-
// For symbols like =, <, > etc
|
|
204
|
-
if (i === 0 || !isAlphanumeric(sql[i - 1])) {
|
|
205
|
-
return i;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return -1;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Parse a value (column, literal, function call, etc.)
|
|
216
|
-
* @param {string} value - Value string
|
|
217
|
-
* @returns {Object} - Parsed value
|
|
218
|
-
*/
|
|
219
|
-
function parseValue(value) {
|
|
220
|
-
value = value.trim();
|
|
221
|
-
|
|
222
|
-
// Strip PostgreSQL type casts (e.g., 'admin'::user_role -> 'admin')
|
|
223
|
-
const typeCastMatch = value.match(/^(.+)::[\w_]+$/);
|
|
224
|
-
if (typeCastMatch) {
|
|
225
|
-
value = typeCastMatch[1].trim();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Handle NULL
|
|
229
|
-
if (value.toUpperCase() === 'NULL') {
|
|
230
|
-
return { type: 'NULL' };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Handle boolean literals
|
|
234
|
-
if (value.toUpperCase() === 'TRUE') {
|
|
235
|
-
return { type: 'BOOLEAN', value: true };
|
|
236
|
-
}
|
|
237
|
-
if (value.toUpperCase() === 'FALSE') {
|
|
238
|
-
return { type: 'BOOLEAN', value: false };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Handle string literals
|
|
242
|
-
if ((value.startsWith("'") && value.endsWith("'")) ||
|
|
243
|
-
(value.startsWith('"') && value.endsWith('"'))) {
|
|
244
|
-
return {
|
|
245
|
-
type: 'STRING',
|
|
246
|
-
value: value.substring(1, value.length - 1)
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Handle numbers
|
|
251
|
-
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
252
|
-
return {
|
|
253
|
-
type: 'NUMBER',
|
|
254
|
-
value: parseFloat(value)
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Handle array literals (for IN operator)
|
|
259
|
-
if (value.startsWith('(') && value.endsWith(')')) {
|
|
260
|
-
const inner = value.substring(1, value.length - 1);
|
|
261
|
-
const items = inner.split(',').map(item => parseValue(item.trim()));
|
|
262
|
-
return {
|
|
263
|
-
type: 'ARRAY',
|
|
264
|
-
items: items
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Handle function calls
|
|
269
|
-
const funcMatch = value.match(/^(\w+)\((.*)\)$/);
|
|
270
|
-
if (funcMatch) {
|
|
271
|
-
return {
|
|
272
|
-
type: 'FUNCTION',
|
|
273
|
-
name: funcMatch[1],
|
|
274
|
-
args: funcMatch[2] ? funcMatch[2].split(',').map(arg => parseValue(arg.trim())) : []
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Handle column references (including table.column)
|
|
279
|
-
if (/^[\w.]+$/.test(value)) {
|
|
280
|
-
const parts = value.split('.');
|
|
281
|
-
if (parts.length === 2) {
|
|
282
|
-
return {
|
|
283
|
-
type: 'COLUMN',
|
|
284
|
-
table: parts[0],
|
|
285
|
-
column: parts[1]
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
return {
|
|
289
|
-
type: 'COLUMN',
|
|
290
|
-
column: value
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Unknown - return as raw
|
|
295
|
-
return {
|
|
296
|
-
type: 'RAW',
|
|
297
|
-
value: value
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Count unmatched parentheses
|
|
303
|
-
*/
|
|
304
|
-
function countParentheses(str) {
|
|
305
|
-
let count = 0;
|
|
306
|
-
for (const char of str) {
|
|
307
|
-
if (char === '(') count++;
|
|
308
|
-
if (char === ')') count--;
|
|
309
|
-
}
|
|
310
|
-
return count;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Check if character is alphanumeric
|
|
315
|
-
*/
|
|
316
|
-
function isAlphanumeric(char) {
|
|
317
|
-
return /[a-zA-Z0-9_]/.test(char);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Convert AST to JavaScript boolean expression
|
|
322
|
-
* @param {Object} ast - Abstract syntax tree
|
|
323
|
-
* @param {string} dataVar - Variable name for data object (e.g., 'data', 'user')
|
|
324
|
-
* @param {string} userVar - Variable name for user object
|
|
325
|
-
* @returns {string} - JavaScript expression
|
|
326
|
-
*/
|
|
327
|
-
function astToJavaScript(ast, dataVar = 'data', userVar = 'user') {
|
|
328
|
-
if (!ast) return 'true';
|
|
329
|
-
|
|
330
|
-
switch (ast.type) {
|
|
331
|
-
case 'OR':
|
|
332
|
-
return '(' + ast.operands.map(op => astToJavaScript(op, dataVar, userVar)).join(' || ') + ')';
|
|
333
|
-
|
|
334
|
-
case 'AND':
|
|
335
|
-
return '(' + ast.operands.map(op => astToJavaScript(op, dataVar, userVar)).join(' && ') + ')';
|
|
336
|
-
|
|
337
|
-
case 'NOT':
|
|
338
|
-
return '!(' + astToJavaScript(ast.operand, dataVar, userVar) + ')';
|
|
339
|
-
|
|
340
|
-
case 'COMPARISON':
|
|
341
|
-
return convertComparisonToJS(ast, dataVar, userVar);
|
|
342
|
-
|
|
343
|
-
case 'COLUMN':
|
|
344
|
-
return valueToJS(ast, dataVar, userVar);
|
|
345
|
-
|
|
346
|
-
case 'BOOLEAN':
|
|
347
|
-
return ast.value.toString();
|
|
348
|
-
|
|
349
|
-
default:
|
|
350
|
-
return 'true';
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Convert comparison to JavaScript
|
|
356
|
-
*/
|
|
357
|
-
function convertComparisonToJS(ast, dataVar, userVar) {
|
|
358
|
-
const left = valueToJS(ast.left, dataVar, userVar);
|
|
359
|
-
const right = ast.right ? valueToJS(ast.right, dataVar, userVar) : null;
|
|
360
|
-
|
|
361
|
-
switch (ast.operator.toUpperCase()) {
|
|
362
|
-
case '=':
|
|
363
|
-
return `${left} === ${right}`;
|
|
364
|
-
case '!=':
|
|
365
|
-
case '<>':
|
|
366
|
-
return `${left} !== ${right}`;
|
|
367
|
-
case '<':
|
|
368
|
-
return `${left} < ${right}`;
|
|
369
|
-
case '>':
|
|
370
|
-
return `${left} > ${right}`;
|
|
371
|
-
case '<=':
|
|
372
|
-
return `${left} <= ${right}`;
|
|
373
|
-
case '>=':
|
|
374
|
-
return `${left} >= ${right}`;
|
|
375
|
-
case 'IN':
|
|
376
|
-
if (ast.right.type === 'ARRAY') {
|
|
377
|
-
const items = ast.right.items.map(item => valueToJS(item, dataVar, userVar)).join(', ');
|
|
378
|
-
return `[${items}].includes(${left})`;
|
|
379
|
-
}
|
|
380
|
-
return `${right}.includes(${left})`;
|
|
381
|
-
case 'NOT IN':
|
|
382
|
-
if (ast.right.type === 'ARRAY') {
|
|
383
|
-
const items = ast.right.items.map(item => valueToJS(item, dataVar, userVar)).join(', ');
|
|
384
|
-
return `![${items}].includes(${left})`;
|
|
385
|
-
}
|
|
386
|
-
return `!${right}.includes(${left})`;
|
|
387
|
-
case 'IS NULL':
|
|
388
|
-
return `${left} == null`;
|
|
389
|
-
case 'IS NOT NULL':
|
|
390
|
-
return `${left} != null`;
|
|
391
|
-
case 'LIKE':
|
|
392
|
-
case 'ILIKE':
|
|
393
|
-
// Convert SQL LIKE to JavaScript regex
|
|
394
|
-
return `${left}?.toString().match(/${sqlLikeToRegex(ast.right.value)}/${ast.operator === 'ILIKE' ? 'i' : ''}) != null`;
|
|
395
|
-
default:
|
|
396
|
-
return 'true';
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Convert value to JavaScript
|
|
402
|
-
*/
|
|
403
|
-
function valueToJS(ast, dataVar, userVar) {
|
|
404
|
-
if (!ast) return 'null';
|
|
405
|
-
|
|
406
|
-
switch (ast.type) {
|
|
407
|
-
case 'COLUMN':
|
|
408
|
-
// Map PostgreSQL special columns
|
|
409
|
-
const column = ast.column.toLowerCase();
|
|
410
|
-
if (column === 'current_user' || column === 'session_user' || column === 'user') {
|
|
411
|
-
return `${userVar}?.id`;
|
|
412
|
-
}
|
|
413
|
-
// Regular column reference
|
|
414
|
-
if (ast.table) {
|
|
415
|
-
return `${dataVar}?.${ast.column}`;
|
|
416
|
-
}
|
|
417
|
-
return `${dataVar}?.${ast.column}`;
|
|
418
|
-
|
|
419
|
-
case 'STRING':
|
|
420
|
-
return `'${ast.value.replace(/'/g, "\\'")}'`;
|
|
421
|
-
|
|
422
|
-
case 'NUMBER':
|
|
423
|
-
return ast.value.toString();
|
|
424
|
-
|
|
425
|
-
case 'BOOLEAN':
|
|
426
|
-
return ast.value.toString();
|
|
427
|
-
|
|
428
|
-
case 'NULL':
|
|
429
|
-
return 'null';
|
|
430
|
-
|
|
431
|
-
case 'FUNCTION':
|
|
432
|
-
return convertFunctionToJS(ast, dataVar, userVar);
|
|
433
|
-
|
|
434
|
-
case 'ARRAY':
|
|
435
|
-
return '[' + ast.items.map(item => valueToJS(item, dataVar, userVar)).join(', ') + ']';
|
|
436
|
-
|
|
437
|
-
case 'SUBQUERY':
|
|
438
|
-
// Subqueries can't be directly converted to JavaScript
|
|
439
|
-
return '[]';
|
|
440
|
-
|
|
441
|
-
default:
|
|
442
|
-
return 'null';
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Convert PostgreSQL function to JavaScript
|
|
448
|
-
*/
|
|
449
|
-
function convertFunctionToJS(ast, dataVar, userVar) {
|
|
450
|
-
const funcName = ast.name.toLowerCase();
|
|
451
|
-
|
|
452
|
-
switch (funcName) {
|
|
453
|
-
case 'current_user':
|
|
454
|
-
case 'session_user':
|
|
455
|
-
return `${userVar}?.id`;
|
|
456
|
-
case 'current_user_id':
|
|
457
|
-
// Custom function: current_user_id() -> user?.id
|
|
458
|
-
return `${userVar}?.id`;
|
|
459
|
-
case 'current_user_role':
|
|
460
|
-
// Custom function: current_user_role() -> user?.role
|
|
461
|
-
return `${userVar}?.role`;
|
|
462
|
-
case 'current_setting':
|
|
463
|
-
// current_setting('app.current_user_id') -> user?.id
|
|
464
|
-
return `${userVar}?.id`;
|
|
465
|
-
case 'auth':
|
|
466
|
-
// auth.uid() -> user?.id (Supabase style)
|
|
467
|
-
if (ast.args[0]?.value === 'uid') {
|
|
468
|
-
return `${userVar}?.id`;
|
|
469
|
-
}
|
|
470
|
-
return `${userVar}?.id`;
|
|
471
|
-
default:
|
|
472
|
-
return 'null';
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Convert SQL LIKE pattern to regex
|
|
478
|
-
*/
|
|
479
|
-
function sqlLikeToRegex(pattern) {
|
|
480
|
-
return pattern
|
|
481
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
482
|
-
.replace(/%/g, '.*') // % becomes .*
|
|
483
|
-
.replace(/_/g, '.'); // _ becomes .
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Convert AST to Prisma filter object
|
|
488
|
-
* @param {Object} ast - Abstract syntax tree
|
|
489
|
-
* @param {string} userVar - Variable name for user object
|
|
490
|
-
* @returns {string} - JavaScript code that returns Prisma filter
|
|
491
|
-
*/
|
|
492
|
-
function astToPrismaFilter(ast, userVar = 'user') {
|
|
493
|
-
if (!ast) return '{}';
|
|
494
|
-
|
|
495
|
-
switch (ast.type) {
|
|
496
|
-
case 'OR':
|
|
497
|
-
const orFilters = ast.operands.map(op => astToPrismaFilter(op, userVar));
|
|
498
|
-
return `{ OR: [${orFilters.join(', ')}] }`;
|
|
499
|
-
|
|
500
|
-
case 'AND':
|
|
501
|
-
const andFilters = ast.operands.map(op => astToPrismaFilter(op, userVar));
|
|
502
|
-
return `{ AND: [${andFilters.join(', ')}] }`;
|
|
503
|
-
|
|
504
|
-
case 'NOT':
|
|
505
|
-
return `{ NOT: ${astToPrismaFilter(ast.operand, userVar)} }`;
|
|
506
|
-
|
|
507
|
-
case 'COMPARISON':
|
|
508
|
-
return convertComparisonToPrisma(ast, userVar);
|
|
509
|
-
|
|
510
|
-
default:
|
|
511
|
-
return '{}';
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Convert comparison to Prisma filter
|
|
517
|
-
*/
|
|
518
|
-
function convertComparisonToPrisma(ast, userVar) {
|
|
519
|
-
if (ast.left.type !== 'COLUMN') return '{}';
|
|
520
|
-
|
|
521
|
-
const column = ast.left.column;
|
|
522
|
-
const rightValue = valueToPrisma(ast.right, userVar);
|
|
523
|
-
|
|
524
|
-
switch (ast.operator.toUpperCase()) {
|
|
525
|
-
case '=':
|
|
526
|
-
return `{ ${column}: ${rightValue} }`;
|
|
527
|
-
case '!=':
|
|
528
|
-
case '<>':
|
|
529
|
-
return `{ ${column}: { not: ${rightValue} } }`;
|
|
530
|
-
case '<':
|
|
531
|
-
return `{ ${column}: { lt: ${rightValue} } }`;
|
|
532
|
-
case '>':
|
|
533
|
-
return `{ ${column}: { gt: ${rightValue} } }`;
|
|
534
|
-
case '<=':
|
|
535
|
-
return `{ ${column}: { lte: ${rightValue} } }`;
|
|
536
|
-
case '>=':
|
|
537
|
-
return `{ ${column}: { gte: ${rightValue} } }`;
|
|
538
|
-
case 'IN':
|
|
539
|
-
if (ast.right.type === 'ARRAY') {
|
|
540
|
-
const items = ast.right.items.map(item => valueToPrisma(item, userVar)).join(', ');
|
|
541
|
-
return `{ ${column}: { in: [${items}] } }`;
|
|
542
|
-
}
|
|
543
|
-
return `{ ${column}: { in: ${rightValue} } }`;
|
|
544
|
-
case 'NOT IN':
|
|
545
|
-
if (ast.right.type === 'ARRAY') {
|
|
546
|
-
const items = ast.right.items.map(item => valueToPrisma(item, userVar)).join(', ');
|
|
547
|
-
return `{ ${column}: { notIn: [${items}] } }`;
|
|
548
|
-
}
|
|
549
|
-
return `{ ${column}: { notIn: ${rightValue} } }`;
|
|
550
|
-
case 'IS NULL':
|
|
551
|
-
return `{ ${column}: null }`;
|
|
552
|
-
case 'IS NOT NULL':
|
|
553
|
-
return `{ ${column}: { not: null } }`;
|
|
554
|
-
case 'LIKE':
|
|
555
|
-
case 'ILIKE':
|
|
556
|
-
return `{ ${column}: { contains: ${rightValue}, mode: '${ast.operator === 'ILIKE' ? 'insensitive' : 'default'}' } }`;
|
|
557
|
-
default:
|
|
558
|
-
return '{}';
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Convert value to Prisma-compatible value
|
|
564
|
-
*/
|
|
565
|
-
function valueToPrisma(ast, userVar) {
|
|
566
|
-
if (!ast) return 'null';
|
|
567
|
-
|
|
568
|
-
switch (ast.type) {
|
|
569
|
-
case 'COLUMN':
|
|
570
|
-
const column = ast.column.toLowerCase();
|
|
571
|
-
if (column === 'current_user' || column === 'session_user' || column === 'user') {
|
|
572
|
-
return `${userVar}?.id`;
|
|
573
|
-
}
|
|
574
|
-
return `${userVar}?.${ast.column}`;
|
|
575
|
-
|
|
576
|
-
case 'STRING':
|
|
577
|
-
return `'${ast.value.replace(/'/g, "\\'")}'`;
|
|
578
|
-
|
|
579
|
-
case 'NUMBER':
|
|
580
|
-
return ast.value.toString();
|
|
581
|
-
|
|
582
|
-
case 'BOOLEAN':
|
|
583
|
-
return ast.value.toString();
|
|
584
|
-
|
|
585
|
-
case 'NULL':
|
|
586
|
-
return 'null';
|
|
587
|
-
|
|
588
|
-
case 'FUNCTION':
|
|
589
|
-
const funcName = ast.name.toLowerCase();
|
|
590
|
-
if (funcName === 'current_user' || funcName === 'session_user' || funcName === 'current_user_id') {
|
|
591
|
-
return `${userVar}?.id`;
|
|
592
|
-
}
|
|
593
|
-
if (funcName === 'current_user_role') {
|
|
594
|
-
return `${userVar}?.role`;
|
|
595
|
-
}
|
|
596
|
-
return `${userVar}?.id`;
|
|
597
|
-
|
|
598
|
-
case 'SUBQUERY':
|
|
599
|
-
// Subqueries can't be directly converted to Prisma
|
|
600
|
-
return '[]';
|
|
601
|
-
|
|
602
|
-
default:
|
|
603
|
-
return 'null';
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
module.exports = {
|
|
608
|
-
parseSqlExpression,
|
|
609
|
-
astToJavaScript,
|
|
610
|
-
astToPrismaFilter
|
|
611
|
-
};
|