@rapidd/build 1.0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/cli.js +30 -0
- package/index.js +11 -0
- package/package.json +63 -0
- package/src/commands/build.js +448 -0
- package/src/generators/modelGenerator.js +168 -0
- package/src/generators/relationshipsGenerator.js +186 -0
- package/src/generators/rlsGenerator.js +334 -0
- package/src/generators/rlsGeneratorV2.js +381 -0
- package/src/generators/routeGenerator.js +127 -0
- package/src/parsers/advancedRLSConverter.js +305 -0
- package/src/parsers/autoRLSConverter.js +322 -0
- package/src/parsers/datasourceParser.js +73 -0
- package/src/parsers/deepSQLAnalyzer.js +540 -0
- package/src/parsers/dynamicRLSConverter.js +353 -0
- package/src/parsers/enhancedRLSConverter.js +181 -0
- package/src/parsers/functionAnalyzer.js +302 -0
- package/src/parsers/postgresRLSConverter.js +192 -0
- package/src/parsers/prismaFilterBuilder.js +422 -0
- package/src/parsers/prismaParser.js +245 -0
- package/src/parsers/sqlToJsConverter.js +611 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced PostgreSQL RLS to JavaScript/Prisma Converter
|
|
3
|
+
* Handles real production PostgreSQL RLS patterns with custom functions
|
|
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
|
+
// Remove extra whitespace and newlines for consistent parsing
|
|
17
|
+
sql = sql.replace(/\s+/g, ' ').replace(/\n/g, ' ');
|
|
18
|
+
|
|
19
|
+
// Handle CASE WHEN expressions
|
|
20
|
+
if (sql.toUpperCase().startsWith('CASE')) {
|
|
21
|
+
return convertCaseWhen(sql, dataVar, userVar);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle combined conditions with OR
|
|
25
|
+
const orMatch = sql.match(/\((.*?)\)\s+OR\s+\((.*?)\)/i);
|
|
26
|
+
if (orMatch) {
|
|
27
|
+
const left = convertToJavaScript(orMatch[1], dataVar, userVar);
|
|
28
|
+
const right = convertToJavaScript(orMatch[2], dataVar, userVar);
|
|
29
|
+
return `(${left} || ${right})`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle role checks: get_current_user_role() = ANY (ARRAY[...])
|
|
33
|
+
if (sql.includes('get_current_user_role')) {
|
|
34
|
+
return convertRoleCheck(sql, userVar);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle field comparisons with custom functions
|
|
38
|
+
const customFuncPatterns = [
|
|
39
|
+
{ pattern: /teacher_id\s*=\s*get_current_teacher_id\(\)/i, replacement: `${dataVar}?.teacher_id === ${userVar}?.teacher_id` },
|
|
40
|
+
{ pattern: /student_id\s*=\s*get_current_student_id\(\)/i, replacement: `${dataVar}?.student_id === ${userVar}?.student_id` },
|
|
41
|
+
{ pattern: /user_id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'[^)]*\)[^)]*\)/i, replacement: `${dataVar}?.user_id === ${userVar}?.id` },
|
|
42
|
+
{ pattern: /id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'[^)]*\)[^)]*\)/i, replacement: `${dataVar}?.id === ${userVar}?.id` },
|
|
43
|
+
{ pattern: /(\w+)\s*=\s*get_current_teacher_id\(\)/i, replacement: (m) => `${dataVar}?.${m[1]} === ${userVar}?.teacher_id` },
|
|
44
|
+
{ pattern: /(\w+)\s*=\s*get_current_student_id\(\)/i, replacement: (m) => `${dataVar}?.${m[1]} === ${userVar}?.student_id` }
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const { pattern, replacement } of customFuncPatterns) {
|
|
48
|
+
if (pattern.test(sql)) {
|
|
49
|
+
if (typeof replacement === 'function') {
|
|
50
|
+
const match = sql.match(pattern);
|
|
51
|
+
return replacement(match);
|
|
52
|
+
}
|
|
53
|
+
return replacement;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle EXISTS subqueries
|
|
58
|
+
if (sql.includes('EXISTS')) {
|
|
59
|
+
return convertExistsSubquery(sql, dataVar, userVar);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fallback for unhandled patterns
|
|
63
|
+
return `true /* Complex RLS expression: ${sql.substring(0, 80)}... */`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert role check expressions
|
|
68
|
+
*/
|
|
69
|
+
function convertRoleCheck(sql, userVar) {
|
|
70
|
+
// Extract roles from ARRAY[...] pattern
|
|
71
|
+
const arrayMatch = sql.match(/ARRAY\s*\[([^\]]+)\]/i);
|
|
72
|
+
if (arrayMatch) {
|
|
73
|
+
const roles = arrayMatch[1]
|
|
74
|
+
.split(',')
|
|
75
|
+
.map(r => r.trim())
|
|
76
|
+
.map(r => r.replace(/::[^,\]]+/g, '')) // Remove type casts (including "character varying")
|
|
77
|
+
.map(r => r.replace(/^'|'$/g, '')) // Remove quotes
|
|
78
|
+
.map(r => `'${r}'`)
|
|
79
|
+
.join(', ');
|
|
80
|
+
return `[${roles}].includes(${userVar}?.role)`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Simple role equality
|
|
84
|
+
const simpleMatch = sql.match(/get_current_user_role\(\)[^=]*=\s*'([^']+)'/i);
|
|
85
|
+
if (simpleMatch) {
|
|
86
|
+
return `${userVar}?.role === '${simpleMatch[1]}'`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `true /* Unparseable role check */`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert CASE WHEN expressions
|
|
94
|
+
*/
|
|
95
|
+
function convertCaseWhen(sql, dataVar, userVar) {
|
|
96
|
+
// Extract the CASE expression and branches
|
|
97
|
+
const caseMatch = sql.match(/CASE\s+(\S+(?:\([^)]*\))?)\s+((?:WHEN[\s\S]+?)+)\s*(?:ELSE\s+([\s\S]+?))?\s*END/i);
|
|
98
|
+
|
|
99
|
+
if (!caseMatch) {
|
|
100
|
+
return `false /* Unparseable CASE expression */`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const caseExpr = caseMatch[1];
|
|
104
|
+
const whenClauses = caseMatch[2];
|
|
105
|
+
const elseClause = caseMatch[3];
|
|
106
|
+
|
|
107
|
+
// Determine what's being tested
|
|
108
|
+
let testExpr = '';
|
|
109
|
+
if (caseExpr.includes('get_current_user_role')) {
|
|
110
|
+
testExpr = `${userVar}?.role`;
|
|
111
|
+
} else if (caseExpr.includes('get_current_teacher_id')) {
|
|
112
|
+
testExpr = `${userVar}?.teacher_id`;
|
|
113
|
+
} else if (caseExpr.includes('get_current_student_id')) {
|
|
114
|
+
testExpr = `${userVar}?.student_id`;
|
|
115
|
+
} else {
|
|
116
|
+
return `false /* Unsupported CASE expression: ${caseExpr} */`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parse WHEN branches
|
|
120
|
+
const whenPattern = /WHEN\s+'?([^':\s]+)'?(?:::\w+)?\s+THEN\s+((?:(?!WHEN\s+|ELSE\s+|END).)+)/gi;
|
|
121
|
+
const conditions = [];
|
|
122
|
+
|
|
123
|
+
let match;
|
|
124
|
+
while ((match = whenPattern.exec(whenClauses)) !== null) {
|
|
125
|
+
const value = match[1];
|
|
126
|
+
const thenExpr = match[2].trim();
|
|
127
|
+
|
|
128
|
+
if (thenExpr.toLowerCase() === 'true') {
|
|
129
|
+
conditions.push(`${testExpr} === '${value}'`);
|
|
130
|
+
} else if (thenExpr.toLowerCase() === 'false') {
|
|
131
|
+
// Skip false conditions
|
|
132
|
+
} else {
|
|
133
|
+
// Complex THEN expression
|
|
134
|
+
const convertedThen = convertThenExpression(thenExpr, dataVar, userVar);
|
|
135
|
+
if (convertedThen !== 'false') {
|
|
136
|
+
conditions.push(`(${testExpr} === '${value}' && ${convertedThen})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle ELSE clause
|
|
142
|
+
if (elseClause && elseClause.trim().toLowerCase() === 'true') {
|
|
143
|
+
conditions.push('true');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return conditions.length > 0 ? conditions.join(' || ') : 'false';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convert THEN expressions in CASE WHEN
|
|
151
|
+
*/
|
|
152
|
+
function convertThenExpression(expr, dataVar, userVar) {
|
|
153
|
+
expr = expr.trim();
|
|
154
|
+
|
|
155
|
+
// Remove parentheses
|
|
156
|
+
if (expr.startsWith('(') && expr.endsWith(')')) {
|
|
157
|
+
expr = expr.substring(1, expr.length - 1).trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle EXISTS
|
|
161
|
+
if (expr.includes('EXISTS')) {
|
|
162
|
+
return convertExistsSubquery(expr, dataVar, userVar);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle field = function() patterns
|
|
166
|
+
const patterns = [
|
|
167
|
+
{ regex: /teacher_id\s*=\s*get_current_teacher_id\(\)/i, js: `${dataVar}?.teacher_id === ${userVar}?.teacher_id` },
|
|
168
|
+
{ regex: /student_id\s*=\s*get_current_student_id\(\)/i, js: `${dataVar}?.student_id === ${userVar}?.student_id` },
|
|
169
|
+
{ regex: /id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'[^)]*\)[^)]*\)/i, js: `${dataVar}?.id === ${userVar}?.id` },
|
|
170
|
+
{ regex: /(\w+)\s*=\s*get_current_(\w+)_id\(\)/i, handler: (m) => `${dataVar}?.${m[1]} === ${userVar}?.${m[2]}_id` }
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
for (const pattern of patterns) {
|
|
174
|
+
if (pattern.regex.test(expr)) {
|
|
175
|
+
if (pattern.handler) {
|
|
176
|
+
const match = expr.match(pattern.regex);
|
|
177
|
+
return pattern.handler(match);
|
|
178
|
+
}
|
|
179
|
+
return pattern.js;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return 'true';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Convert EXISTS subqueries to JavaScript
|
|
188
|
+
* These typically check relationships between tables
|
|
189
|
+
*/
|
|
190
|
+
function convertExistsSubquery(sql, dataVar, userVar) {
|
|
191
|
+
// Common patterns in EXISTS subqueries
|
|
192
|
+
|
|
193
|
+
// Pattern 1: Check if student_tariff belongs to current student
|
|
194
|
+
if (sql.includes('student_tariff') && sql.includes('get_current_student_id')) {
|
|
195
|
+
return `true /* TODO: Check if ${dataVar}?.student_tariff.student_id === ${userVar}?.student_id */`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Pattern 2: Check if teacher has access
|
|
199
|
+
if (sql.includes('get_current_teacher_id')) {
|
|
200
|
+
return `true /* TODO: Check if ${dataVar} is accessible to teacher ${userVar}?.teacher_id */`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Pattern 3: Check user address relationships
|
|
204
|
+
if (sql.includes('contact_address_id') || sql.includes('billing_address_id')) {
|
|
205
|
+
return `true /* TODO: Check if address belongs to user via contact_address_id or billing_address_id */`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Pattern 4: Generic student access check
|
|
209
|
+
if (sql.includes('student') && sql.includes('get_current_student_id')) {
|
|
210
|
+
return `true /* TODO: Check student relationship */`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Default EXISTS handling
|
|
214
|
+
return `true /* EXISTS subquery requires manual implementation */`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Convert to Prisma filter
|
|
219
|
+
*/
|
|
220
|
+
function convertToPrismaFilter(sql, userVar = 'user') {
|
|
221
|
+
if (!sql || sql.trim() === '') {
|
|
222
|
+
return '{}';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
sql = sql.trim().replace(/\s+/g, ' ').replace(/\n/g, ' ');
|
|
226
|
+
|
|
227
|
+
// Role-based filters can't be directly applied in Prisma WHERE clause
|
|
228
|
+
if (sql.includes('get_current_user_role')) {
|
|
229
|
+
return '{}'; // Role checks are done in hasAccess, not in filter
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Handle CASE WHEN - extract filterable conditions
|
|
233
|
+
if (sql.toUpperCase().startsWith('CASE')) {
|
|
234
|
+
return convertCaseWhenToPrisma(sql, userVar);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle simple field comparisons
|
|
238
|
+
const patterns = [
|
|
239
|
+
{ regex: /teacher_id\s*=\s*get_current_teacher_id\(\)/i, filter: `{ teacher_id: ${userVar}?.teacher_id }` },
|
|
240
|
+
{ regex: /student_id\s*=\s*get_current_student_id\(\)/i, filter: `{ student_id: ${userVar}?.student_id }` },
|
|
241
|
+
{ regex: /user_id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i, filter: `{ user_id: ${userVar}?.id }` },
|
|
242
|
+
{ regex: /^id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i, filter: `{ id: ${userVar}?.id }` }
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
for (const pattern of patterns) {
|
|
246
|
+
if (pattern.regex.test(sql)) {
|
|
247
|
+
return pattern.filter;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Handle OR conditions
|
|
252
|
+
const orMatch = sql.match(/\((.*?)\)\s+OR\s+\((.*?)\)/i);
|
|
253
|
+
if (orMatch) {
|
|
254
|
+
const left = convertToPrismaFilter(orMatch[1], userVar);
|
|
255
|
+
const right = convertToPrismaFilter(orMatch[2], userVar);
|
|
256
|
+
if (left !== '{}' && right !== '{}') {
|
|
257
|
+
return `{ OR: [${left}, ${right}] }`;
|
|
258
|
+
} else if (left !== '{}') {
|
|
259
|
+
return left;
|
|
260
|
+
} else if (right !== '{}') {
|
|
261
|
+
return right;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return '{}';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Convert CASE WHEN to Prisma filter
|
|
270
|
+
*/
|
|
271
|
+
function convertCaseWhenToPrisma(sql, userVar) {
|
|
272
|
+
// Extract field-based conditions that can be filtered in Prisma
|
|
273
|
+
const filters = [];
|
|
274
|
+
|
|
275
|
+
// Look for teacher_id = get_current_teacher_id()
|
|
276
|
+
if (sql.includes('teacher_id') && sql.includes('get_current_teacher_id')) {
|
|
277
|
+
filters.push(`{ teacher_id: ${userVar}?.teacher_id }`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Look for student_id = get_current_student_id()
|
|
281
|
+
if (sql.includes('student_id') && sql.includes('get_current_student_id')) {
|
|
282
|
+
filters.push(`{ student_id: ${userVar}?.student_id }`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Look for id = current_setting('app.current_user_id')
|
|
286
|
+
if (sql.match(/id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i)) {
|
|
287
|
+
filters.push(`{ id: ${userVar}?.id }`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (filters.length === 0) {
|
|
291
|
+
return '{}';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (filters.length === 1) {
|
|
295
|
+
return filters[0];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Multiple conditions are OR'd in CASE WHEN
|
|
299
|
+
return `{ OR: [${filters.join(', ')}] }`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
convertToJavaScript,
|
|
304
|
+
convertToPrismaFilter
|
|
305
|
+
};
|
|
@@ -0,0 +1,322 @@
|
|
|
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 };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Load .env file if it exists
|
|
5
|
+
try {
|
|
6
|
+
require('dotenv').config({ path: path.join(process.cwd(), '.env') });
|
|
7
|
+
} catch (e) {
|
|
8
|
+
// dotenv not available, skip
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse datasource configuration from Prisma schema
|
|
13
|
+
* @param {string} schemaPath - Path to Prisma schema file
|
|
14
|
+
* @returns {Object} - Datasource configuration with resolved URL
|
|
15
|
+
*/
|
|
16
|
+
function parseDatasource(schemaPath) {
|
|
17
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
18
|
+
|
|
19
|
+
// Extract datasource block
|
|
20
|
+
const datasourceRegex = /datasource\s+\w+\s*{([^}]*)}/;
|
|
21
|
+
const match = schemaContent.match(datasourceRegex);
|
|
22
|
+
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error('No datasource block found in Prisma schema');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const datasourceBlock = match[1];
|
|
28
|
+
|
|
29
|
+
// Extract provider
|
|
30
|
+
const providerMatch = datasourceBlock.match(/provider\s*=\s*"([^"]+)"/);
|
|
31
|
+
const provider = providerMatch ? providerMatch[1] : null;
|
|
32
|
+
|
|
33
|
+
// Extract url
|
|
34
|
+
const urlMatch = datasourceBlock.match(/url\s*=\s*(.+)/);
|
|
35
|
+
if (!urlMatch) {
|
|
36
|
+
throw new Error('No url found in datasource block');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let url = urlMatch[1].trim();
|
|
40
|
+
|
|
41
|
+
// Handle env() function
|
|
42
|
+
const envMatch = url.match(/env\(["']([^"']+)["']\)/);
|
|
43
|
+
if (envMatch) {
|
|
44
|
+
const envVar = envMatch[1];
|
|
45
|
+
url = process.env[envVar];
|
|
46
|
+
|
|
47
|
+
if (!url) {
|
|
48
|
+
throw new Error(`Environment variable ${envVar} is not defined`);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Remove quotes if present
|
|
52
|
+
url = url.replace(/^["']|["']$/g, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Detect PostgreSQL from provider OR from the actual connection URL
|
|
56
|
+
// This is important because the schema might say "mysql" but DATABASE_URL could be postgresql://
|
|
57
|
+
let isPostgreSQL = provider === 'postgresql' || provider === 'postgres';
|
|
58
|
+
|
|
59
|
+
if (!isPostgreSQL && url) {
|
|
60
|
+
// Check if URL starts with postgresql:// or postgres://
|
|
61
|
+
isPostgreSQL = url.startsWith('postgresql://') || url.startsWith('postgres://');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
provider,
|
|
66
|
+
url,
|
|
67
|
+
isPostgreSQL
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
parseDatasource
|
|
73
|
+
};
|