@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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Function Analyzer
|
|
3
|
+
* Dynamically analyzes PostgreSQL functions and generates JavaScript mappings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Client } = require('pg');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Analyze all PostgreSQL functions used in RLS policies
|
|
10
|
+
* @param {string} databaseUrl - PostgreSQL connection URL
|
|
11
|
+
* @returns {Object} - Function mappings and metadata
|
|
12
|
+
*/
|
|
13
|
+
async function analyzeFunctions(databaseUrl) {
|
|
14
|
+
if (!databaseUrl) {
|
|
15
|
+
return {
|
|
16
|
+
functionMappings: {},
|
|
17
|
+
sessionVariables: [],
|
|
18
|
+
userContextRequirements: {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await client.connect();
|
|
26
|
+
|
|
27
|
+
// Step 1: Find all functions used in RLS policies
|
|
28
|
+
const functionsQuery = await client.query(`
|
|
29
|
+
WITH rls_text AS (
|
|
30
|
+
SELECT
|
|
31
|
+
qual || ' ' || COALESCE(with_check, '') as policy_text
|
|
32
|
+
FROM pg_policies
|
|
33
|
+
WHERE schemaname = 'public'
|
|
34
|
+
),
|
|
35
|
+
function_names AS (
|
|
36
|
+
SELECT DISTINCT
|
|
37
|
+
unnest(
|
|
38
|
+
regexp_matches(
|
|
39
|
+
policy_text,
|
|
40
|
+
'(\\w+)\\s*\\(',
|
|
41
|
+
'g'
|
|
42
|
+
)
|
|
43
|
+
) as function_name
|
|
44
|
+
FROM rls_text
|
|
45
|
+
)
|
|
46
|
+
SELECT function_name
|
|
47
|
+
FROM function_names
|
|
48
|
+
WHERE function_name NOT IN ('SELECT', 'EXISTS', 'ANY', 'ARRAY', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'THEN', 'ELSE', 'CASE', 'WHEN', 'END')
|
|
49
|
+
AND function_name NOT LIKE 'current_setting%'
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
const functionNames = functionsQuery.rows.map(r => r.function_name);
|
|
53
|
+
|
|
54
|
+
// Step 2: Analyze each function's definition
|
|
55
|
+
const functionMappings = {};
|
|
56
|
+
const userContextRequirements = {};
|
|
57
|
+
const sessionVariables = new Set();
|
|
58
|
+
|
|
59
|
+
for (const funcName of functionNames) {
|
|
60
|
+
try {
|
|
61
|
+
const funcDef = await client.query(`
|
|
62
|
+
SELECT
|
|
63
|
+
proname as name,
|
|
64
|
+
prosrc as source,
|
|
65
|
+
pg_get_functiondef(oid) as full_definition,
|
|
66
|
+
prorettype::regtype as return_type
|
|
67
|
+
FROM pg_proc
|
|
68
|
+
WHERE proname = $1
|
|
69
|
+
AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
|
|
70
|
+
`, [funcName]);
|
|
71
|
+
|
|
72
|
+
if (funcDef.rows.length > 0) {
|
|
73
|
+
const func = funcDef.rows[0];
|
|
74
|
+
const analysis = analyzeFunctionBody(func.source, func.return_type);
|
|
75
|
+
|
|
76
|
+
functionMappings[funcName] = analysis.mapping;
|
|
77
|
+
|
|
78
|
+
// Track what this function requires
|
|
79
|
+
if (analysis.requiresUserId) {
|
|
80
|
+
userContextRequirements.id = true;
|
|
81
|
+
}
|
|
82
|
+
if (analysis.queriesTable) {
|
|
83
|
+
userContextRequirements[analysis.returnField] = {
|
|
84
|
+
table: analysis.queriesTable,
|
|
85
|
+
lookupField: analysis.lookupField || 'user_id',
|
|
86
|
+
description: `${funcName}() queries ${analysis.queriesTable} table`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Track session variables
|
|
91
|
+
analysis.sessionVars.forEach(v => sessionVariables.add(v));
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.warn(`Could not analyze function ${funcName}:`, e.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Step 3: Find all session variables used
|
|
99
|
+
const settingsQuery = await client.query(`
|
|
100
|
+
SELECT DISTINCT
|
|
101
|
+
unnest(
|
|
102
|
+
regexp_matches(
|
|
103
|
+
pg_policies.qual || ' ' || COALESCE(pg_policies.with_check, ''),
|
|
104
|
+
'current_setting\\s*\\(\\s*''([^'']+)''',
|
|
105
|
+
'g'
|
|
106
|
+
)
|
|
107
|
+
) as setting_name
|
|
108
|
+
FROM pg_policies
|
|
109
|
+
WHERE schemaname = 'public'
|
|
110
|
+
`);
|
|
111
|
+
|
|
112
|
+
settingsQuery.rows.forEach(r => sessionVariables.add(r.setting_name));
|
|
113
|
+
|
|
114
|
+
await client.end();
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
functionMappings,
|
|
118
|
+
sessionVariables: Array.from(sessionVariables),
|
|
119
|
+
userContextRequirements
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
} catch (error) {
|
|
123
|
+
try {
|
|
124
|
+
await client.end();
|
|
125
|
+
} catch (e) {}
|
|
126
|
+
|
|
127
|
+
console.warn('Could not analyze PostgreSQL functions:', error.message);
|
|
128
|
+
return {
|
|
129
|
+
functionMappings: {},
|
|
130
|
+
sessionVariables: [],
|
|
131
|
+
userContextRequirements: {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Analyze a PostgreSQL function body to understand what it does
|
|
138
|
+
* @param {string} functionBody - The PL/pgSQL function source
|
|
139
|
+
* @param {string} returnType - The return type of the function
|
|
140
|
+
* @returns {Object} - Analysis results
|
|
141
|
+
*/
|
|
142
|
+
function analyzeFunctionBody(functionBody, returnType) {
|
|
143
|
+
const analysis = {
|
|
144
|
+
mapping: null,
|
|
145
|
+
requiresUserId: false,
|
|
146
|
+
queriesTable: null,
|
|
147
|
+
returnField: null,
|
|
148
|
+
lookupField: null,
|
|
149
|
+
sessionVars: []
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (!functionBody) return analysis;
|
|
153
|
+
|
|
154
|
+
// Detect session variable usage
|
|
155
|
+
const sessionMatches = functionBody.matchAll(/current_setting\s*\(\s*'([^']+)'/gi);
|
|
156
|
+
for (const match of sessionMatches) {
|
|
157
|
+
analysis.sessionVars.push(match[1]);
|
|
158
|
+
if (match[1].includes('user_id')) {
|
|
159
|
+
analysis.requiresUserId = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Detect SELECT statements to understand what table is queried
|
|
164
|
+
const selectMatch = functionBody.match(/SELECT\s+(\w+)(?:\.(\w+))?\s+INTO\s+\w+\s+FROM\s+(\w+)/i);
|
|
165
|
+
if (selectMatch) {
|
|
166
|
+
const fieldOrAlias = selectMatch[1];
|
|
167
|
+
const fieldName = selectMatch[2] || fieldOrAlias;
|
|
168
|
+
const tableName = selectMatch[3];
|
|
169
|
+
|
|
170
|
+
analysis.queriesTable = tableName;
|
|
171
|
+
|
|
172
|
+
// Infer the return field name
|
|
173
|
+
if (fieldName === 'id' || fieldOrAlias === 'id') {
|
|
174
|
+
analysis.returnField = `${tableName}_id`;
|
|
175
|
+
} else {
|
|
176
|
+
analysis.returnField = fieldName;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Detect the lookup condition
|
|
180
|
+
const whereMatch = functionBody.match(/WHERE\s+\w+\.(\w+)\s*=\s*current_setting/i);
|
|
181
|
+
if (whereMatch) {
|
|
182
|
+
analysis.lookupField = whereMatch[1];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Generate JavaScript mapping
|
|
186
|
+
if (returnType.includes('int')) {
|
|
187
|
+
analysis.mapping = {
|
|
188
|
+
type: 'lookup',
|
|
189
|
+
returns: `user?.${analysis.returnField}`,
|
|
190
|
+
description: `Looks up ${tableName}.id where ${tableName}.${analysis.lookupField} = current_user_id`
|
|
191
|
+
};
|
|
192
|
+
} else if (returnType.includes('char') || returnType.includes('text')) {
|
|
193
|
+
// For string returns (like role), map directly
|
|
194
|
+
const userTableMatch = functionBody.match(/FROM\s+["']?user["']?/i);
|
|
195
|
+
if (userTableMatch) {
|
|
196
|
+
// Querying user table directly
|
|
197
|
+
analysis.mapping = {
|
|
198
|
+
type: 'direct',
|
|
199
|
+
returns: `user?.${analysis.returnField}`,
|
|
200
|
+
description: `Returns user.${analysis.returnField}`
|
|
201
|
+
};
|
|
202
|
+
} else {
|
|
203
|
+
analysis.mapping = {
|
|
204
|
+
type: 'lookup',
|
|
205
|
+
returns: `user?.${analysis.returnField}`,
|
|
206
|
+
description: `Looks up ${tableName}.${analysis.returnField}`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// If we couldn't parse it, make a best guess based on function patterns
|
|
213
|
+
if (!analysis.mapping) {
|
|
214
|
+
const funcName = functionBody.match(/FUNCTION\s+(\w+)/i)?.[1] || '';
|
|
215
|
+
|
|
216
|
+
if (funcName.includes('role')) {
|
|
217
|
+
analysis.mapping = {
|
|
218
|
+
type: 'inferred',
|
|
219
|
+
returns: 'user?.role',
|
|
220
|
+
description: 'Inferred from function name'
|
|
221
|
+
};
|
|
222
|
+
} else if (funcName.includes('_id')) {
|
|
223
|
+
const entity = funcName.replace(/get_current_|_id/gi, '');
|
|
224
|
+
analysis.mapping = {
|
|
225
|
+
type: 'inferred',
|
|
226
|
+
returns: `user?.${entity}_id`,
|
|
227
|
+
description: 'Inferred from function name'
|
|
228
|
+
};
|
|
229
|
+
} else {
|
|
230
|
+
analysis.mapping = {
|
|
231
|
+
type: 'unknown',
|
|
232
|
+
returns: 'null',
|
|
233
|
+
description: 'Could not analyze function'
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return analysis;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate a function mapping configuration
|
|
243
|
+
* This can be saved to a file for manual adjustment if needed
|
|
244
|
+
*/
|
|
245
|
+
function generateMappingConfig(analysisResult) {
|
|
246
|
+
const config = {
|
|
247
|
+
// Metadata
|
|
248
|
+
generated: new Date().toISOString(),
|
|
249
|
+
source: 'PostgreSQL function analysis',
|
|
250
|
+
|
|
251
|
+
// Function mappings
|
|
252
|
+
functions: {},
|
|
253
|
+
|
|
254
|
+
// Session variable mappings
|
|
255
|
+
sessionVariables: {},
|
|
256
|
+
|
|
257
|
+
// User context requirements
|
|
258
|
+
userContext: {
|
|
259
|
+
required: [],
|
|
260
|
+
optional: [],
|
|
261
|
+
relationships: {}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Build function mappings
|
|
266
|
+
for (const [funcName, mapping] of Object.entries(analysisResult.functionMappings)) {
|
|
267
|
+
config.functions[funcName] = {
|
|
268
|
+
javascript: mapping.returns,
|
|
269
|
+
description: mapping.description,
|
|
270
|
+
type: mapping.type
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Build session variable mappings
|
|
275
|
+
for (const varName of analysisResult.sessionVariables) {
|
|
276
|
+
if (varName.includes('user_id')) {
|
|
277
|
+
config.sessionVariables[varName] = 'user.id';
|
|
278
|
+
} else {
|
|
279
|
+
// Infer from variable name
|
|
280
|
+
const key = varName.split('.').pop().replace(/_/g, '');
|
|
281
|
+
config.sessionVariables[varName] = `user.${key}`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Build user context requirements
|
|
286
|
+
for (const [field, requirement] of Object.entries(analysisResult.userContextRequirements)) {
|
|
287
|
+
if (field === 'id') {
|
|
288
|
+
config.userContext.required.push('id');
|
|
289
|
+
} else {
|
|
290
|
+
config.userContext.relationships[field] = requirement;
|
|
291
|
+
config.userContext.optional.push(field);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return config;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
analyzeFunctions,
|
|
300
|
+
analyzeFunctionBody,
|
|
301
|
+
generateMappingConfig
|
|
302
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL RLS to JavaScript/Prisma Converter
|
|
3
|
+
* Handles real-world PostgreSQL RLS patterns including CASE WHEN, EXISTS, type casts, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert PostgreSQL RLS expression to JavaScript
|
|
8
|
+
* @param {string} sql - PostgreSQL RLS expression
|
|
9
|
+
* @param {string} dataVar - Variable name for row data
|
|
10
|
+
* @param {string} userVar - Variable name for user context
|
|
11
|
+
* @returns {string} - JavaScript boolean expression
|
|
12
|
+
*/
|
|
13
|
+
function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
14
|
+
if (!sql || sql.trim() === '') {
|
|
15
|
+
return 'true';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
sql = sql.trim();
|
|
19
|
+
|
|
20
|
+
// Handle CASE WHEN expressions
|
|
21
|
+
if (sql.toUpperCase().startsWith('CASE')) {
|
|
22
|
+
return convertCaseWhen(sql, dataVar, userVar);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle simple role checks: get_current_user_role() = ANY (ARRAY[...])
|
|
26
|
+
const roleAnyMatch = sql.match(/get_current_user_role\(\)[^=]*=\s*ANY\s*\(\s*(?:\()?ARRAY\s*\[([^\]]+)\]/i);
|
|
27
|
+
if (roleAnyMatch) {
|
|
28
|
+
const roles = roleAnyMatch[1]
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(r => r.trim())
|
|
31
|
+
.map(r => r.replace(/::[^,\]]+/g, '')) // Remove type casts
|
|
32
|
+
.map(r => r.replace(/^'|'$/g, '')) // Remove quotes
|
|
33
|
+
.map(r => `'${r}'`)
|
|
34
|
+
.join(', ');
|
|
35
|
+
return `[${roles}].includes(${userVar}?.role)`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Handle EXISTS subqueries - these need manual implementation
|
|
39
|
+
if (sql.toUpperCase().includes('EXISTS')) {
|
|
40
|
+
return `true /* EXISTS subquery - requires manual implementation: ${sql.substring(0, 50)}... */`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Handle simple comparisons with current_setting
|
|
44
|
+
const settingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i);
|
|
45
|
+
if (settingMatch) {
|
|
46
|
+
const field = settingMatch[1];
|
|
47
|
+
return `${dataVar}?.${field} === ${userVar}?.id`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fallback: return true with comment
|
|
51
|
+
return `true /* Complex RLS - manual implementation needed: ${sql.substring(0, 50)}... */`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert CASE WHEN expression to JavaScript
|
|
56
|
+
*/
|
|
57
|
+
function convertCaseWhen(sql, dataVar, userVar) {
|
|
58
|
+
// Extract CASE conditions - improved regex to handle multiline
|
|
59
|
+
const casePattern = /CASE\s+(\w+\([^)]*\))\s+(WHEN\s+[\s\S]+?)(?:ELSE\s+([\s\S]+?))?END/i;
|
|
60
|
+
const match = sql.match(casePattern);
|
|
61
|
+
|
|
62
|
+
if (!match) {
|
|
63
|
+
return `false /* Unparseable CASE expression */`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const caseExpr = match[1]; // e.g., get_current_user_role()
|
|
67
|
+
const whenClauses = match[2];
|
|
68
|
+
const elseClause = match[3];
|
|
69
|
+
|
|
70
|
+
// Determine what function is being called
|
|
71
|
+
let jsExpr = '';
|
|
72
|
+
if (caseExpr.includes('get_current_user_role')) {
|
|
73
|
+
jsExpr = `${userVar}?.role`;
|
|
74
|
+
} else if (caseExpr.includes('current_user_id')) {
|
|
75
|
+
jsExpr = `${userVar}?.id`;
|
|
76
|
+
} else {
|
|
77
|
+
return `false /* Unsupported CASE expression: ${caseExpr} */`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Parse WHEN clauses - improved regex for multiline THEN
|
|
81
|
+
const whenPattern = /WHEN\s+'([^']+)'(?:::\w+)?\s+THEN\s+([\s\S]+?)(?=\s+WHEN\s+|$)/gi;
|
|
82
|
+
const whenMatches = [...whenClauses.matchAll(whenPattern)];
|
|
83
|
+
const conditions = [];
|
|
84
|
+
|
|
85
|
+
for (const whenMatch of whenMatches) {
|
|
86
|
+
const value = whenMatch[1]; // e.g., 'super_admin'
|
|
87
|
+
let thenExpr = whenMatch[2].trim(); // e.g., 'true' or complex expression
|
|
88
|
+
|
|
89
|
+
// Remove trailing text before next WHEN or ELSE
|
|
90
|
+
thenExpr = thenExpr.replace(/\s+(?:WHEN|ELSE)[\s\S]*$/, '').trim();
|
|
91
|
+
|
|
92
|
+
if (thenExpr.toLowerCase() === 'true') {
|
|
93
|
+
conditions.push(`${jsExpr} === '${value}'`);
|
|
94
|
+
} else {
|
|
95
|
+
// Complex THEN expression - try to convert it
|
|
96
|
+
const convertedThen = convertSimpleExpression(thenExpr, dataVar, userVar);
|
|
97
|
+
conditions.push(`(${jsExpr} === '${value}' && ${convertedThen})`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (conditions.length === 0) {
|
|
102
|
+
return 'false';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return conditions.join(' || ');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Convert simple PostgreSQL expressions
|
|
110
|
+
*/
|
|
111
|
+
function convertSimpleExpression(expr, dataVar, userVar) {
|
|
112
|
+
expr = expr.trim();
|
|
113
|
+
|
|
114
|
+
// Remove outer parentheses
|
|
115
|
+
if (expr.startsWith('(') && expr.endsWith(')')) {
|
|
116
|
+
expr = expr.substring(1, expr.length - 1).trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle EXISTS - mark as needing manual implementation (check FIRST before simple patterns)
|
|
120
|
+
if (expr.toUpperCase().includes('EXISTS')) {
|
|
121
|
+
return `true /* EXISTS subquery requires manual implementation */`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle: id = (current_setting('app.current_user_id')::integer)
|
|
125
|
+
const settingMatch = expr.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'[^)]*\)[^)]*\)/i);
|
|
126
|
+
if (settingMatch) {
|
|
127
|
+
const field = settingMatch[1];
|
|
128
|
+
return `${dataVar}?.${field} === ${userVar}?.id`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return 'true /* Requires manual implementation */';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Convert PostgreSQL RLS expression to Prisma filter
|
|
136
|
+
* @param {string} sql - PostgreSQL RLS expression
|
|
137
|
+
* @param {string} userVar - Variable name for user context
|
|
138
|
+
* @returns {string} - Prisma filter object as string
|
|
139
|
+
*/
|
|
140
|
+
function convertToPrismaFilter(sql, userVar = 'user') {
|
|
141
|
+
if (!sql || sql.trim() === '') {
|
|
142
|
+
return '{}';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
sql = sql.trim();
|
|
146
|
+
|
|
147
|
+
// Handle CASE WHEN - convert to OR filter
|
|
148
|
+
if (sql.toUpperCase().startsWith('CASE')) {
|
|
149
|
+
return convertCaseWhenToPrisma(sql, userVar);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle simple role checks - can't filter on user role in Prisma, return empty
|
|
153
|
+
const roleAnyMatch = sql.match(/get_current_user_role\(\)[^=]*=\s*ANY\s*\(\s*(?:\()?ARRAY\s*\[([^\]]+)\]/i);
|
|
154
|
+
if (roleAnyMatch) {
|
|
155
|
+
// Role-based access can't be filtered in Prisma - must be checked in hasAccess
|
|
156
|
+
return '{}';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle simple comparisons with current_setting
|
|
160
|
+
const settingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i);
|
|
161
|
+
if (settingMatch) {
|
|
162
|
+
const field = settingMatch[1];
|
|
163
|
+
return `{ ${field}: ${userVar}?.id }`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback
|
|
167
|
+
return '{}';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert CASE WHEN to Prisma OR filter
|
|
172
|
+
*/
|
|
173
|
+
function convertCaseWhenToPrisma(sql, userVar) {
|
|
174
|
+
// For CASE WHEN expressions, extract simple field comparisons
|
|
175
|
+
const settingMatches = [...sql.matchAll(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/gi)];
|
|
176
|
+
|
|
177
|
+
if (settingMatches.length > 0) {
|
|
178
|
+
const filters = settingMatches.map(m => `{ ${m[1]}: ${userVar}?.id }`);
|
|
179
|
+
if (filters.length === 1) {
|
|
180
|
+
return filters[0];
|
|
181
|
+
}
|
|
182
|
+
return `{ OR: [${filters.join(', ')}] }`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// If no simple filters found, return empty (role-based checks handled in hasAccess)
|
|
186
|
+
return '{}';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
convertToJavaScript,
|
|
191
|
+
convertToPrismaFilter
|
|
192
|
+
};
|