@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.
@@ -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
+ };