@rapidd/build 1.1.0 → 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 +2 -5
- package/package.json +1 -1
- package/src/commands/build.js +1 -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,322 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Automatic PostgreSQL RLS to JavaScript Converter
|
|
3
|
-
* Uses dynamic function analysis - no hardcoding!
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Create a converter with analyzed function mappings
|
|
8
|
-
*/
|
|
9
|
-
function createConverter(functionMappings = {}, sessionVariables = {}) {
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Convert PostgreSQL RLS to JavaScript
|
|
13
|
-
*/
|
|
14
|
-
function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
15
|
-
if (!sql || sql.trim() === '') {
|
|
16
|
-
return 'true';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
sql = sql.trim().replace(/\s+/g, ' ').replace(/\n/g, ' ');
|
|
20
|
-
|
|
21
|
-
// Handle CASE WHEN
|
|
22
|
-
if (sql.toUpperCase().startsWith('CASE')) {
|
|
23
|
-
return convertCaseWhen(sql, dataVar, userVar);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Handle OR
|
|
27
|
-
const orParts = splitLogicalOperator(sql, 'OR');
|
|
28
|
-
if (orParts.length > 1) {
|
|
29
|
-
return orParts.map(part => convertToJavaScript(part, dataVar, userVar)).join(' || ');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Handle AND
|
|
33
|
-
const andParts = splitLogicalOperator(sql, 'AND');
|
|
34
|
-
if (andParts.length > 1) {
|
|
35
|
-
return '(' + andParts.map(part => convertToJavaScript(part, dataVar, userVar)).join(' && ') + ')';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Handle function = ANY (ARRAY[...])
|
|
39
|
-
const funcAnyMatch = sql.match(/(\w+)\s*\([^)]*\)[^=]*=\s*ANY\s*\(\s*(?:\()?ARRAY\s*\[([^\]]+)\]/i);
|
|
40
|
-
if (funcAnyMatch) {
|
|
41
|
-
const funcName = funcAnyMatch[1];
|
|
42
|
-
const arrayValues = extractArrayValues(funcAnyMatch[2]);
|
|
43
|
-
|
|
44
|
-
// Use analyzed mapping or fallback
|
|
45
|
-
const jsExpression = getFunctionMapping(funcName, userVar);
|
|
46
|
-
return `[${arrayValues}].includes(${jsExpression})`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Handle function() = 'value' (e.g., get_current_user_role() = 'admin')
|
|
50
|
-
const funcValueMatch = sql.match(/(\w+)\s*\([^)]*\)\s*=\s*'([^']+)'/i);
|
|
51
|
-
if (funcValueMatch) {
|
|
52
|
-
const funcName = funcValueMatch[1];
|
|
53
|
-
const value = funcValueMatch[2];
|
|
54
|
-
|
|
55
|
-
const jsExpression = getFunctionMapping(funcName, userVar);
|
|
56
|
-
return `${jsExpression} === '${value}'`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Handle field = function()
|
|
60
|
-
const fieldFuncMatch = sql.match(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/i);
|
|
61
|
-
if (fieldFuncMatch) {
|
|
62
|
-
const field = fieldFuncMatch[1];
|
|
63
|
-
const funcName = fieldFuncMatch[2];
|
|
64
|
-
|
|
65
|
-
const jsExpression = getFunctionMapping(funcName, userVar);
|
|
66
|
-
return `${dataVar}?.${field} === ${jsExpression}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Handle current_setting
|
|
70
|
-
const settingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
|
|
71
|
-
if (settingMatch) {
|
|
72
|
-
const field = settingMatch[1];
|
|
73
|
-
const setting = settingMatch[2];
|
|
74
|
-
|
|
75
|
-
const jsExpression = getSessionMapping(setting, userVar);
|
|
76
|
-
return `${dataVar}?.${field} === ${jsExpression}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle EXISTS
|
|
80
|
-
if (sql.includes('EXISTS')) {
|
|
81
|
-
// Try to extract table and conditions for better TODO comment
|
|
82
|
-
const tableMatch = sql.match(/FROM\s+(\w+)/i);
|
|
83
|
-
const table = tableMatch ? tableMatch[1] : 'related_table';
|
|
84
|
-
return `true /* TODO: Check ${table} relationship */`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Handle boolean literals
|
|
88
|
-
if (sql.toLowerCase() === 'true') return 'true';
|
|
89
|
-
if (sql.toLowerCase() === 'false') return 'false';
|
|
90
|
-
|
|
91
|
-
return `true /* Unhandled: ${sql.substring(0, 50)}... */`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get JavaScript mapping for a PostgreSQL function
|
|
96
|
-
*/
|
|
97
|
-
function getFunctionMapping(funcName, userVar) {
|
|
98
|
-
// Check analyzed mappings first
|
|
99
|
-
if (functionMappings[funcName]) {
|
|
100
|
-
const mapping = functionMappings[funcName];
|
|
101
|
-
if (mapping.javascript) {
|
|
102
|
-
// Replace 'user' placeholder with actual variable name
|
|
103
|
-
return mapping.javascript.replace(/user/g, userVar);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Special case for role functions - they should map to user?.role not user?.user_role
|
|
108
|
-
if (funcName.toLowerCase() === 'get_current_user_role' ||
|
|
109
|
-
funcName.toLowerCase() === 'current_user_role') {
|
|
110
|
-
return `${userVar}?.role`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Fallback: infer from function name
|
|
114
|
-
// get_current_X -> user?.X
|
|
115
|
-
const getCurrentMatch = funcName.match(/get_current_(\w+)/i);
|
|
116
|
-
if (getCurrentMatch) {
|
|
117
|
-
return `${userVar}?.${getCurrentMatch[1]}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// current_X -> user?.X
|
|
121
|
-
const currentMatch = funcName.match(/current_(\w+)/i);
|
|
122
|
-
if (currentMatch) {
|
|
123
|
-
return `${userVar}?.${currentMatch[1]}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Unknown function
|
|
127
|
-
return `${userVar}?.${funcName}()`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Get JavaScript mapping for a session variable
|
|
132
|
-
*/
|
|
133
|
-
function getSessionMapping(setting, userVar) {
|
|
134
|
-
// Check analyzed mappings
|
|
135
|
-
if (sessionVariables[setting]) {
|
|
136
|
-
return sessionVariables[setting].replace(/user/g, userVar);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Fallback: infer from setting name
|
|
140
|
-
if (setting.includes('user_id')) {
|
|
141
|
-
return `${userVar}?.id`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const parts = setting.split('.');
|
|
145
|
-
const lastPart = parts[parts.length - 1];
|
|
146
|
-
return `${userVar}?.${lastPart}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Convert CASE WHEN
|
|
151
|
-
*/
|
|
152
|
-
function convertCaseWhen(sql, dataVar, userVar) {
|
|
153
|
-
const caseMatch = sql.match(/CASE\s+([^W]+)\s+((?:WHEN[\s\S]+?)+)(?:\s+ELSE\s+([\s\S]+?))?\s*END/i);
|
|
154
|
-
if (!caseMatch) return 'false';
|
|
155
|
-
|
|
156
|
-
const caseExpr = caseMatch[1].trim();
|
|
157
|
-
const whenClauses = caseMatch[2];
|
|
158
|
-
const elseClause = caseMatch[3];
|
|
159
|
-
|
|
160
|
-
// Extract function name from CASE expression
|
|
161
|
-
const funcMatch = caseExpr.match(/(\w+)\s*\(/);
|
|
162
|
-
const testExpression = funcMatch ? getFunctionMapping(funcMatch[1], userVar) : caseExpr;
|
|
163
|
-
|
|
164
|
-
const conditions = [];
|
|
165
|
-
const whenPattern = /WHEN\s+'?([^':\s]+)'?(?:::\w+)?\s+THEN\s+((?:(?!WHEN|ELSE|END).)+)/gi;
|
|
166
|
-
|
|
167
|
-
let match;
|
|
168
|
-
while ((match = whenPattern.exec(whenClauses)) !== null) {
|
|
169
|
-
const value = match[1];
|
|
170
|
-
const thenExpr = match[2].trim();
|
|
171
|
-
|
|
172
|
-
if (thenExpr.toLowerCase() === 'true') {
|
|
173
|
-
conditions.push(`${testExpression} === '${value}'`);
|
|
174
|
-
} else if (thenExpr.toLowerCase() !== 'false') {
|
|
175
|
-
const thenJs = convertToJavaScript(thenExpr, dataVar, userVar);
|
|
176
|
-
conditions.push(`(${testExpression} === '${value}' && ${thenJs})`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (elseClause && elseClause.trim().toLowerCase() !== 'false') {
|
|
181
|
-
conditions.push(convertToJavaScript(elseClause.trim(), dataVar, userVar));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return conditions.length > 0 ? `(${conditions.join(' || ')})` : 'false';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Convert to Prisma filter
|
|
189
|
-
*/
|
|
190
|
-
function convertToPrismaFilter(sql, userVar = 'user') {
|
|
191
|
-
if (!sql || sql.trim() === '') return '{}';
|
|
192
|
-
|
|
193
|
-
sql = sql.trim().replace(/\s+/g, ' ').replace(/\n/g, ' ');
|
|
194
|
-
|
|
195
|
-
// Handle OR conditions
|
|
196
|
-
const orParts = splitLogicalOperator(sql, 'OR');
|
|
197
|
-
if (orParts.length > 1) {
|
|
198
|
-
const orFilters = orParts
|
|
199
|
-
.map(part => convertToPrismaFilter(part, userVar))
|
|
200
|
-
.filter(f => f !== '{}');
|
|
201
|
-
|
|
202
|
-
if (orFilters.length === 0) return '{}';
|
|
203
|
-
if (orFilters.length === 1) return orFilters[0];
|
|
204
|
-
return `{ OR: [${orFilters.join(', ')}] }`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Handle AND conditions
|
|
208
|
-
const andParts = splitLogicalOperator(sql, 'AND');
|
|
209
|
-
if (andParts.length > 1) {
|
|
210
|
-
const andFilters = andParts
|
|
211
|
-
.map(part => convertToPrismaFilter(part, userVar))
|
|
212
|
-
.filter(f => f !== '{}');
|
|
213
|
-
|
|
214
|
-
if (andFilters.length === 0) return '{}';
|
|
215
|
-
if (andFilters.length === 1) return andFilters[0];
|
|
216
|
-
return `{ AND: [${andFilters.join(', ')}] }`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Skip role-only checks (they can't be filtered in Prisma)
|
|
220
|
-
if (sql.match(/^\s*(?:get_current_user_role|current_user_role)\s*\(\)/i)) {
|
|
221
|
-
return '{}';
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Extract field comparisons
|
|
225
|
-
const filters = [];
|
|
226
|
-
|
|
227
|
-
// Handle: field = function()
|
|
228
|
-
const fieldFuncMatch = sql.match(/^(\w+)\s*=\s*(\w+)\s*\([^)]*\)$/i);
|
|
229
|
-
if (fieldFuncMatch) {
|
|
230
|
-
const field = fieldFuncMatch[1];
|
|
231
|
-
const funcName = fieldFuncMatch[2];
|
|
232
|
-
|
|
233
|
-
// Skip role functions
|
|
234
|
-
if (funcName.toLowerCase().includes('role')) {
|
|
235
|
-
return '{}';
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const jsExpression = getFunctionMapping(funcName, userVar);
|
|
239
|
-
return `{ ${field}: ${jsExpression} }`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Handle: field = (current_setting('...'))
|
|
243
|
-
const settingMatch = sql.match(/^(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
|
|
244
|
-
if (settingMatch) {
|
|
245
|
-
const field = settingMatch[1];
|
|
246
|
-
const setting = settingMatch[2];
|
|
247
|
-
const jsExpression = getSessionMapping(setting, userVar);
|
|
248
|
-
return `{ ${field}: ${jsExpression} }`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Handle: field = 'value' (direct comparison)
|
|
252
|
-
const directMatch = sql.match(/^(\w+)\s*=\s*'([^']+)'/i);
|
|
253
|
-
if (directMatch) {
|
|
254
|
-
const field = directMatch[1];
|
|
255
|
-
const value = directMatch[2];
|
|
256
|
-
return `{ ${field}: '${value}' }`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Handle: field = number
|
|
260
|
-
const numberMatch = sql.match(/^(\w+)\s*=\s*(\d+)$/i);
|
|
261
|
-
if (numberMatch) {
|
|
262
|
-
const field = numberMatch[1];
|
|
263
|
-
const value = numberMatch[2];
|
|
264
|
-
return `{ ${field}: ${value} }`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Can't convert complex conditions to Prisma filters
|
|
268
|
-
return '{}';
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return { convertToJavaScript, convertToPrismaFilter };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Helper: Split by logical operator respecting parentheses
|
|
276
|
-
*/
|
|
277
|
-
function splitLogicalOperator(sql, operator) {
|
|
278
|
-
const parts = [];
|
|
279
|
-
let current = '';
|
|
280
|
-
let depth = 0;
|
|
281
|
-
let inQuotes = false;
|
|
282
|
-
let i = 0;
|
|
283
|
-
|
|
284
|
-
while (i < sql.length) {
|
|
285
|
-
if (sql[i] === "'" && !inQuotes) {
|
|
286
|
-
inQuotes = true;
|
|
287
|
-
} else if (sql[i] === "'" && inQuotes) {
|
|
288
|
-
inQuotes = false;
|
|
289
|
-
} else if (!inQuotes) {
|
|
290
|
-
if (sql[i] === '(') depth++;
|
|
291
|
-
else if (sql[i] === ')') depth--;
|
|
292
|
-
else if (depth === 0) {
|
|
293
|
-
const upcoming = sql.substring(i, i + operator.length + 2);
|
|
294
|
-
if (upcoming.match(new RegExp(`\\s+${operator}\\s+`, 'i'))) {
|
|
295
|
-
if (current.trim()) parts.push(current.trim());
|
|
296
|
-
current = '';
|
|
297
|
-
i += upcoming.match(/\s+\w+\s+/)[0].length - 1;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
current += sql[i];
|
|
302
|
-
i++;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (current.trim()) parts.push(current.trim());
|
|
306
|
-
return parts.length > 1 ? parts : [sql];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Helper: Extract array values
|
|
311
|
-
*/
|
|
312
|
-
function extractArrayValues(arrayContent) {
|
|
313
|
-
return arrayContent
|
|
314
|
-
.split(',')
|
|
315
|
-
.map(r => r.trim())
|
|
316
|
-
.map(r => r.replace(/::[^,\]]+/g, ''))
|
|
317
|
-
.map(r => r.replace(/^'|'$/g, ''))
|
|
318
|
-
.map(r => `'${r}'`)
|
|
319
|
-
.join(', ');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
module.exports = { createConverter };
|
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dynamic PostgreSQL RLS to JavaScript/Prisma Converter
|
|
3
|
-
* Handles any PostgreSQL RLS pattern without hardcoding assumptions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Main converter function - PostgreSQL RLS to JavaScript
|
|
8
|
-
*/
|
|
9
|
-
function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
10
|
-
if (!sql || sql.trim() === '') {
|
|
11
|
-
return 'true';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
sql = sql.trim();
|
|
15
|
-
|
|
16
|
-
// Strip outer parentheses if they wrap the entire expression
|
|
17
|
-
if (sql.startsWith('(') && sql.endsWith(')')) {
|
|
18
|
-
let depth = 0;
|
|
19
|
-
let matchesOuter = true;
|
|
20
|
-
for (let i = 0; i < sql.length; i++) {
|
|
21
|
-
if (sql[i] === '(') depth++;
|
|
22
|
-
if (sql[i] === ')') depth--;
|
|
23
|
-
if (depth === 0 && i < sql.length - 1) {
|
|
24
|
-
matchesOuter = false;
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (matchesOuter) {
|
|
29
|
-
sql = sql.substring(1, sql.length - 1).trim();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Normalize whitespace
|
|
34
|
-
sql = sql.replace(/\s+/g, ' ').replace(/\n/g, ' ');
|
|
35
|
-
|
|
36
|
-
// Handle CASE WHEN expressions
|
|
37
|
-
if (sql.toUpperCase().startsWith('CASE')) {
|
|
38
|
-
return convertCaseWhen(sql, dataVar, userVar);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Handle OR conditions
|
|
42
|
-
const orMatch = sql.match(/\((.*?)\)\s+OR\s+\((.*?)\)/i);
|
|
43
|
-
if (orMatch) {
|
|
44
|
-
const left = convertToJavaScript(orMatch[1], dataVar, userVar);
|
|
45
|
-
const right = convertToJavaScript(orMatch[2], dataVar, userVar);
|
|
46
|
-
return `(${left} || ${right})`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Handle AND conditions
|
|
50
|
-
const andMatch = sql.match(/\((.*?)\)\s+AND\s+\((.*?)\)/i);
|
|
51
|
-
if (andMatch) {
|
|
52
|
-
const left = convertToJavaScript(andMatch[1], dataVar, userVar);
|
|
53
|
-
const right = convertToJavaScript(andMatch[2], dataVar, userVar);
|
|
54
|
-
return `(${left} && ${right})`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Handle function calls with = ANY (ARRAY[...])
|
|
58
|
-
const anyArrayMatch = sql.match(/(\w+)\s*\([^)]*\)[^=]*=\s*ANY\s*\(\s*(?:\()?ARRAY\s*\[([^\]]+)\]/i);
|
|
59
|
-
if (anyArrayMatch) {
|
|
60
|
-
const funcName = anyArrayMatch[1];
|
|
61
|
-
const arrayValues = extractArrayValues(anyArrayMatch[2]);
|
|
62
|
-
|
|
63
|
-
// Map function names to user properties dynamically
|
|
64
|
-
const userProperty = mapFunctionToUserProperty(funcName);
|
|
65
|
-
return `[${arrayValues}].includes(${userVar}?.${userProperty})`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Handle field = function() comparisons (with or without quotes)
|
|
69
|
-
const funcCompareMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*(\w+)\s*\([^)]*\)/i);
|
|
70
|
-
if (funcCompareMatch) {
|
|
71
|
-
const field = funcCompareMatch[1];
|
|
72
|
-
const funcName = funcCompareMatch[2];
|
|
73
|
-
|
|
74
|
-
// Map function to user property
|
|
75
|
-
const userProperty = mapFunctionToUserProperty(funcName);
|
|
76
|
-
return `${dataVar}?.${field} === ${userVar}?.${userProperty}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle field = (current_setting(...))
|
|
80
|
-
const currentSettingMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
|
|
81
|
-
if (currentSettingMatch) {
|
|
82
|
-
const field = currentSettingMatch[1];
|
|
83
|
-
const setting = currentSettingMatch[2];
|
|
84
|
-
|
|
85
|
-
// Map setting to user property
|
|
86
|
-
const userProperty = mapSettingToUserProperty(setting);
|
|
87
|
-
return `${dataVar}?.${field} === ${userVar}?.${userProperty}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Handle field = literal value (true, false, numbers, strings)
|
|
91
|
-
const literalMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*(true|false|\d+|'[^']+')/i);
|
|
92
|
-
if (literalMatch) {
|
|
93
|
-
const field = literalMatch[1];
|
|
94
|
-
const value = literalMatch[2];
|
|
95
|
-
return `${dataVar}?.${field} === ${value}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Handle EXISTS subqueries
|
|
99
|
-
if (sql.includes('EXISTS')) {
|
|
100
|
-
return handleExistsSubquery(sql, dataVar, userVar);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Handle simple boolean values
|
|
104
|
-
if (sql.toLowerCase() === 'true') return 'true';
|
|
105
|
-
if (sql.toLowerCase() === 'false') return 'false';
|
|
106
|
-
|
|
107
|
-
// Unhandled pattern
|
|
108
|
-
console.warn(`⚠ Unhandled RLS pattern: ${sql}`);
|
|
109
|
-
return `true /* Unhandled RLS pattern: ${sql.substring(0, 60)}... */`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Map PostgreSQL function names to user object properties
|
|
114
|
-
* This is where customization happens based on your PostgreSQL functions
|
|
115
|
-
*/
|
|
116
|
-
function mapFunctionToUserProperty(funcName) {
|
|
117
|
-
const mappings = {
|
|
118
|
-
// Common patterns - can be customized per project
|
|
119
|
-
'get_current_user_role': 'role',
|
|
120
|
-
'get_current_user_id': 'id',
|
|
121
|
-
'current_user_id': 'id',
|
|
122
|
-
'get_current_teacher_id': 'teacher_id',
|
|
123
|
-
'get_current_student_id': 'student_id',
|
|
124
|
-
'get_current_agency_id': 'agency_id',
|
|
125
|
-
'get_current_school_id': 'school_id',
|
|
126
|
-
// Add more mappings as needed
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// If we have a mapping, use it
|
|
130
|
-
if (mappings[funcName]) {
|
|
131
|
-
return mappings[funcName];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Try to infer from function name
|
|
135
|
-
// get_current_X_id -> X_id
|
|
136
|
-
const getIdMatch = funcName.match(/get_current_(\w+)_id/i);
|
|
137
|
-
if (getIdMatch) {
|
|
138
|
-
return `${getIdMatch[1]}_id`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// get_current_X -> X
|
|
142
|
-
const getCurrentMatch = funcName.match(/get_current_(\w+)/i);
|
|
143
|
-
if (getCurrentMatch) {
|
|
144
|
-
return getCurrentMatch[1];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// current_X -> X
|
|
148
|
-
const currentMatch = funcName.match(/current_(\w+)/i);
|
|
149
|
-
if (currentMatch) {
|
|
150
|
-
return currentMatch[1];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Default: use function name as-is
|
|
154
|
-
return funcName;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Map PostgreSQL settings to user properties
|
|
159
|
-
*/
|
|
160
|
-
function mapSettingToUserProperty(setting) {
|
|
161
|
-
// app.current_user_id -> id
|
|
162
|
-
if (setting === 'app.current_user_id') {
|
|
163
|
-
return 'id';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// app.current_X -> X
|
|
167
|
-
const appMatch = setting.match(/app\.current_(\w+)/i);
|
|
168
|
-
if (appMatch) {
|
|
169
|
-
return appMatch[1];
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Default
|
|
173
|
-
return setting.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Extract and format array values
|
|
178
|
-
*/
|
|
179
|
-
function extractArrayValues(arrayContent) {
|
|
180
|
-
return arrayContent
|
|
181
|
-
.split(',')
|
|
182
|
-
.map(r => r.trim())
|
|
183
|
-
.map(r => r.replace(/::[^,\]]+/g, '')) // Remove all type casts
|
|
184
|
-
.map(r => r.replace(/^'|'$/g, '')) // Remove quotes
|
|
185
|
-
.map(r => `'${r}'`)
|
|
186
|
-
.join(', ');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Convert CASE WHEN expressions
|
|
191
|
-
*/
|
|
192
|
-
function convertCaseWhen(sql, dataVar, userVar) {
|
|
193
|
-
const caseMatch = sql.match(/CASE\s+(\S+(?:\([^)]*\))?)\s+((?:WHEN[\s\S]+?)+)\s*(?:ELSE\s+([\s\S]+?))?\s*END/i);
|
|
194
|
-
|
|
195
|
-
if (!caseMatch) {
|
|
196
|
-
return `false /* Unparseable CASE expression */`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const caseExpr = caseMatch[1];
|
|
200
|
-
const whenClauses = caseMatch[2];
|
|
201
|
-
const elseClause = caseMatch[3];
|
|
202
|
-
|
|
203
|
-
// Extract the function being called in CASE
|
|
204
|
-
let testProperty = 'unknown';
|
|
205
|
-
const funcMatch = caseExpr.match(/(\w+)\s*\(/);
|
|
206
|
-
if (funcMatch) {
|
|
207
|
-
testProperty = mapFunctionToUserProperty(funcMatch[1]);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Parse WHEN branches
|
|
211
|
-
const whenPattern = /WHEN\s+'?([^':\s]+)'?(?:::\w+)?\s+THEN\s+((?:(?!WHEN\s+|ELSE\s+|END).)+)/gi;
|
|
212
|
-
const conditions = [];
|
|
213
|
-
|
|
214
|
-
let match;
|
|
215
|
-
while ((match = whenPattern.exec(whenClauses)) !== null) {
|
|
216
|
-
const value = match[1];
|
|
217
|
-
const thenExpr = match[2].trim();
|
|
218
|
-
|
|
219
|
-
if (thenExpr.toLowerCase() === 'true') {
|
|
220
|
-
conditions.push(`${userVar}?.${testProperty} === '${value}'`);
|
|
221
|
-
} else if (thenExpr.toLowerCase() === 'false') {
|
|
222
|
-
// Skip false conditions
|
|
223
|
-
} else {
|
|
224
|
-
// Complex THEN expression - recursively convert
|
|
225
|
-
const convertedThen = convertToJavaScript(thenExpr, dataVar, userVar);
|
|
226
|
-
if (convertedThen !== 'false') {
|
|
227
|
-
conditions.push(`(${userVar}?.${testProperty} === '${value}' && ${convertedThen})`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Handle ELSE clause
|
|
233
|
-
if (elseClause && elseClause.trim().toLowerCase() !== 'false') {
|
|
234
|
-
const elseConverted = convertToJavaScript(elseClause.trim(), dataVar, userVar);
|
|
235
|
-
if (elseConverted !== 'false') {
|
|
236
|
-
conditions.push(elseConverted);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return conditions.length > 0 ? `(${conditions.join(' || ')})` : 'false';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Handle EXISTS subqueries
|
|
245
|
-
* These typically check relationships and need manual implementation
|
|
246
|
-
*/
|
|
247
|
-
function handleExistsSubquery(sql, dataVar, userVar) {
|
|
248
|
-
// Try to extract meaningful information from the EXISTS clause
|
|
249
|
-
const existsMatch = sql.match(/EXISTS\s*\(\s*SELECT[^)]+FROM\s+(\w+)[^)]+WHERE\s+([^)]+)\)/i);
|
|
250
|
-
|
|
251
|
-
if (existsMatch) {
|
|
252
|
-
const tableName = existsMatch[1];
|
|
253
|
-
const whereClause = existsMatch[2];
|
|
254
|
-
|
|
255
|
-
// Look for common patterns in WHERE clause
|
|
256
|
-
// Pattern: checking if related table's foreign key matches current context
|
|
257
|
-
const fkMatch = whereClause.match(/(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)/gi);
|
|
258
|
-
if (fkMatch) {
|
|
259
|
-
return `true /* TODO: Check ${tableName} relationship via JOIN */`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Pattern: checking against current user functions
|
|
263
|
-
if (whereClause.match(/get_current_\w+_id\(\)/i)) {
|
|
264
|
-
return `true /* TODO: Verify ${tableName} access for current user context */`;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return `true /* EXISTS subquery needs manual implementation */`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Convert to Prisma filter
|
|
273
|
-
*/
|
|
274
|
-
function convertToPrismaFilter(sql, userVar = 'user') {
|
|
275
|
-
if (!sql || sql.trim() === '') {
|
|
276
|
-
return '{}';
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
sql = sql.trim().replace(/\s+/g, ' ').replace(/\n/g, ' ');
|
|
280
|
-
|
|
281
|
-
// CASE WHEN - extract filterable conditions
|
|
282
|
-
if (sql.toUpperCase().startsWith('CASE')) {
|
|
283
|
-
return extractPrismaFiltersFromCase(sql, userVar);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Handle field = function() patterns
|
|
287
|
-
const funcCompareMatch = sql.match(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/i);
|
|
288
|
-
if (funcCompareMatch) {
|
|
289
|
-
const field = funcCompareMatch[1];
|
|
290
|
-
const funcName = funcCompareMatch[2];
|
|
291
|
-
const userProperty = mapFunctionToUserProperty(funcName);
|
|
292
|
-
|
|
293
|
-
// Only create filter if it's a data field comparison
|
|
294
|
-
if (!funcName.includes('role')) {
|
|
295
|
-
return `{ ${field}: ${userVar}?.${userProperty} }`;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Handle current_setting patterns
|
|
300
|
-
const settingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
|
|
301
|
-
if (settingMatch) {
|
|
302
|
-
const field = settingMatch[1];
|
|
303
|
-
const setting = settingMatch[2];
|
|
304
|
-
const userProperty = mapSettingToUserProperty(setting);
|
|
305
|
-
return `{ ${field}: ${userVar}?.${userProperty} }`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Handle OR conditions
|
|
309
|
-
const orMatch = sql.match(/\((.*?)\)\s+OR\s+\((.*?)\)/i);
|
|
310
|
-
if (orMatch) {
|
|
311
|
-
const left = convertToPrismaFilter(orMatch[1], userVar);
|
|
312
|
-
const right = convertToPrismaFilter(orMatch[2], userVar);
|
|
313
|
-
|
|
314
|
-
if (left !== '{}' && right !== '{}') {
|
|
315
|
-
return `{ OR: [${left}, ${right}] }`;
|
|
316
|
-
} else if (left !== '{}') {
|
|
317
|
-
return left;
|
|
318
|
-
} else if (right !== '{}') {
|
|
319
|
-
return right;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Role-based checks can't be filtered in Prisma
|
|
324
|
-
if (sql.match(/get_current_\w+_role/i) || sql.includes('= ANY')) {
|
|
325
|
-
return '{}';
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return '{}';
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Extract Prisma filters from CASE WHEN
|
|
333
|
-
*/
|
|
334
|
-
function extractPrismaFiltersFromCase(sql, userVar) {
|
|
335
|
-
const filters = new Set();
|
|
336
|
-
|
|
337
|
-
// Look for all field = function() patterns in the CASE statement
|
|
338
|
-
const fieldMatches = sql.matchAll(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/gi);
|
|
339
|
-
|
|
340
|
-
for (const match of fieldMatches) {
|
|
341
|
-
const field = match[1];
|
|
342
|
-
const funcName = match[2];
|
|
343
|
-
|
|
344
|
-
// Skip role checks
|
|
345
|
-
if (funcName.includes('role')) continue;
|
|
346
|
-
|
|
347
|
-
const userProperty = mapFunctionToUserProperty(funcName);
|
|
348
|
-
filters.add(`{ ${field}: ${userVar}?.${userProperty} }`);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Look for current_setting patterns
|
|
352
|
-
const settingMatches = sql.matchAll(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/gi);
|
|
353
|
-
|
|
354
|
-
for (const match of settingMatches) {
|
|
355
|
-
const field = match[1];
|
|
356
|
-
const setting = match[2];
|
|
357
|
-
const userProperty = mapSettingToUserProperty(setting);
|
|
358
|
-
filters.add(`{ ${field}: ${userVar}?.${userProperty} }`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const filterArray = Array.from(filters);
|
|
362
|
-
|
|
363
|
-
if (filterArray.length === 0) {
|
|
364
|
-
return '{}';
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (filterArray.length === 1) {
|
|
368
|
-
return filterArray[0];
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return `{ OR: [${filterArray.join(', ')}] }`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
module.exports = {
|
|
375
|
-
convertToJavaScript,
|
|
376
|
-
convertToPrismaFilter,
|
|
377
|
-
mapFunctionToUserProperty,
|
|
378
|
-
mapSettingToUserProperty
|
|
379
|
-
};
|