@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,353 @@
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
+ // Normalize whitespace
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 OR conditions
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 AND conditions
33
+ const andMatch = sql.match(/\((.*?)\)\s+AND\s+\((.*?)\)/i);
34
+ if (andMatch) {
35
+ const left = convertToJavaScript(andMatch[1], dataVar, userVar);
36
+ const right = convertToJavaScript(andMatch[2], dataVar, userVar);
37
+ return `(${left} && ${right})`;
38
+ }
39
+
40
+ // Handle function calls with = ANY (ARRAY[...])
41
+ const anyArrayMatch = sql.match(/(\w+)\s*\([^)]*\)[^=]*=\s*ANY\s*\(\s*(?:\()?ARRAY\s*\[([^\]]+)\]/i);
42
+ if (anyArrayMatch) {
43
+ const funcName = anyArrayMatch[1];
44
+ const arrayValues = extractArrayValues(anyArrayMatch[2]);
45
+
46
+ // Map function names to user properties dynamically
47
+ const userProperty = mapFunctionToUserProperty(funcName);
48
+ return `[${arrayValues}].includes(${userVar}?.${userProperty})`;
49
+ }
50
+
51
+ // Handle field = function() comparisons
52
+ const funcCompareMatch = sql.match(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/i);
53
+ if (funcCompareMatch) {
54
+ const field = funcCompareMatch[1];
55
+ const funcName = funcCompareMatch[2];
56
+
57
+ // Map function to user property
58
+ const userProperty = mapFunctionToUserProperty(funcName);
59
+ return `${dataVar}?.${field} === ${userVar}?.${userProperty}`;
60
+ }
61
+
62
+ // Handle field = (current_setting(...))
63
+ const currentSettingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
64
+ if (currentSettingMatch) {
65
+ const field = currentSettingMatch[1];
66
+ const setting = currentSettingMatch[2];
67
+
68
+ // Map setting to user property
69
+ const userProperty = mapSettingToUserProperty(setting);
70
+ return `${dataVar}?.${field} === ${userVar}?.${userProperty}`;
71
+ }
72
+
73
+ // Handle EXISTS subqueries
74
+ if (sql.includes('EXISTS')) {
75
+ return handleExistsSubquery(sql, dataVar, userVar);
76
+ }
77
+
78
+ // Handle simple boolean values
79
+ if (sql.toLowerCase() === 'true') return 'true';
80
+ if (sql.toLowerCase() === 'false') return 'false';
81
+
82
+ // Unhandled pattern
83
+ return `true /* Unhandled RLS pattern: ${sql.substring(0, 60)}... */`;
84
+ }
85
+
86
+ /**
87
+ * Map PostgreSQL function names to user object properties
88
+ * This is where customization happens based on your PostgreSQL functions
89
+ */
90
+ function mapFunctionToUserProperty(funcName) {
91
+ const mappings = {
92
+ // Common patterns - can be customized per project
93
+ 'get_current_user_role': 'role',
94
+ 'get_current_user_id': 'id',
95
+ 'current_user_id': 'id',
96
+ 'get_current_teacher_id': 'teacher_id',
97
+ 'get_current_student_id': 'student_id',
98
+ 'get_current_agency_id': 'agency_id',
99
+ 'get_current_school_id': 'school_id',
100
+ // Add more mappings as needed
101
+ };
102
+
103
+ // If we have a mapping, use it
104
+ if (mappings[funcName]) {
105
+ return mappings[funcName];
106
+ }
107
+
108
+ // Try to infer from function name
109
+ // get_current_X_id -> X_id
110
+ const getIdMatch = funcName.match(/get_current_(\w+)_id/i);
111
+ if (getIdMatch) {
112
+ return `${getIdMatch[1]}_id`;
113
+ }
114
+
115
+ // get_current_X -> X
116
+ const getCurrentMatch = funcName.match(/get_current_(\w+)/i);
117
+ if (getCurrentMatch) {
118
+ return getCurrentMatch[1];
119
+ }
120
+
121
+ // current_X -> X
122
+ const currentMatch = funcName.match(/current_(\w+)/i);
123
+ if (currentMatch) {
124
+ return currentMatch[1];
125
+ }
126
+
127
+ // Default: use function name as-is
128
+ return funcName;
129
+ }
130
+
131
+ /**
132
+ * Map PostgreSQL settings to user properties
133
+ */
134
+ function mapSettingToUserProperty(setting) {
135
+ // app.current_user_id -> id
136
+ if (setting === 'app.current_user_id') {
137
+ return 'id';
138
+ }
139
+
140
+ // app.current_X -> X
141
+ const appMatch = setting.match(/app\.current_(\w+)/i);
142
+ if (appMatch) {
143
+ return appMatch[1];
144
+ }
145
+
146
+ // Default
147
+ return setting.replace(/[^a-zA-Z0-9_]/g, '_');
148
+ }
149
+
150
+ /**
151
+ * Extract and format array values
152
+ */
153
+ function extractArrayValues(arrayContent) {
154
+ return arrayContent
155
+ .split(',')
156
+ .map(r => r.trim())
157
+ .map(r => r.replace(/::[^,\]]+/g, '')) // Remove all type casts
158
+ .map(r => r.replace(/^'|'$/g, '')) // Remove quotes
159
+ .map(r => `'${r}'`)
160
+ .join(', ');
161
+ }
162
+
163
+ /**
164
+ * Convert CASE WHEN expressions
165
+ */
166
+ function convertCaseWhen(sql, dataVar, userVar) {
167
+ const caseMatch = sql.match(/CASE\s+(\S+(?:\([^)]*\))?)\s+((?:WHEN[\s\S]+?)+)\s*(?:ELSE\s+([\s\S]+?))?\s*END/i);
168
+
169
+ if (!caseMatch) {
170
+ return `false /* Unparseable CASE expression */`;
171
+ }
172
+
173
+ const caseExpr = caseMatch[1];
174
+ const whenClauses = caseMatch[2];
175
+ const elseClause = caseMatch[3];
176
+
177
+ // Extract the function being called in CASE
178
+ let testProperty = 'unknown';
179
+ const funcMatch = caseExpr.match(/(\w+)\s*\(/);
180
+ if (funcMatch) {
181
+ testProperty = mapFunctionToUserProperty(funcMatch[1]);
182
+ }
183
+
184
+ // Parse WHEN branches
185
+ const whenPattern = /WHEN\s+'?([^':\s]+)'?(?:::\w+)?\s+THEN\s+((?:(?!WHEN\s+|ELSE\s+|END).)+)/gi;
186
+ const conditions = [];
187
+
188
+ let match;
189
+ while ((match = whenPattern.exec(whenClauses)) !== null) {
190
+ const value = match[1];
191
+ const thenExpr = match[2].trim();
192
+
193
+ if (thenExpr.toLowerCase() === 'true') {
194
+ conditions.push(`${userVar}?.${testProperty} === '${value}'`);
195
+ } else if (thenExpr.toLowerCase() === 'false') {
196
+ // Skip false conditions
197
+ } else {
198
+ // Complex THEN expression - recursively convert
199
+ const convertedThen = convertToJavaScript(thenExpr, dataVar, userVar);
200
+ if (convertedThen !== 'false') {
201
+ conditions.push(`(${userVar}?.${testProperty} === '${value}' && ${convertedThen})`);
202
+ }
203
+ }
204
+ }
205
+
206
+ // Handle ELSE clause
207
+ if (elseClause && elseClause.trim().toLowerCase() !== 'false') {
208
+ const elseConverted = convertToJavaScript(elseClause.trim(), dataVar, userVar);
209
+ if (elseConverted !== 'false') {
210
+ conditions.push(elseConverted);
211
+ }
212
+ }
213
+
214
+ return conditions.length > 0 ? `(${conditions.join(' || ')})` : 'false';
215
+ }
216
+
217
+ /**
218
+ * Handle EXISTS subqueries
219
+ * These typically check relationships and need manual implementation
220
+ */
221
+ function handleExistsSubquery(sql, dataVar, userVar) {
222
+ // Try to extract meaningful information from the EXISTS clause
223
+ const existsMatch = sql.match(/EXISTS\s*\(\s*SELECT[^)]+FROM\s+(\w+)[^)]+WHERE\s+([^)]+)\)/i);
224
+
225
+ if (existsMatch) {
226
+ const tableName = existsMatch[1];
227
+ const whereClause = existsMatch[2];
228
+
229
+ // Look for common patterns in WHERE clause
230
+ // Pattern: checking if related table's foreign key matches current context
231
+ const fkMatch = whereClause.match(/(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)/gi);
232
+ if (fkMatch) {
233
+ return `true /* TODO: Check ${tableName} relationship via JOIN */`;
234
+ }
235
+
236
+ // Pattern: checking against current user functions
237
+ if (whereClause.match(/get_current_\w+_id\(\)/i)) {
238
+ return `true /* TODO: Verify ${tableName} access for current user context */`;
239
+ }
240
+ }
241
+
242
+ return `true /* EXISTS subquery needs manual implementation */`;
243
+ }
244
+
245
+ /**
246
+ * Convert to Prisma filter
247
+ */
248
+ function convertToPrismaFilter(sql, userVar = 'user') {
249
+ if (!sql || sql.trim() === '') {
250
+ return '{}';
251
+ }
252
+
253
+ sql = sql.trim().replace(/\s+/g, ' ').replace(/\n/g, ' ');
254
+
255
+ // CASE WHEN - extract filterable conditions
256
+ if (sql.toUpperCase().startsWith('CASE')) {
257
+ return extractPrismaFiltersFromCase(sql, userVar);
258
+ }
259
+
260
+ // Handle field = function() patterns
261
+ const funcCompareMatch = sql.match(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/i);
262
+ if (funcCompareMatch) {
263
+ const field = funcCompareMatch[1];
264
+ const funcName = funcCompareMatch[2];
265
+ const userProperty = mapFunctionToUserProperty(funcName);
266
+
267
+ // Only create filter if it's a data field comparison
268
+ if (!funcName.includes('role')) {
269
+ return `{ ${field}: ${userVar}?.${userProperty} }`;
270
+ }
271
+ }
272
+
273
+ // Handle current_setting patterns
274
+ const settingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
275
+ if (settingMatch) {
276
+ const field = settingMatch[1];
277
+ const setting = settingMatch[2];
278
+ const userProperty = mapSettingToUserProperty(setting);
279
+ return `{ ${field}: ${userVar}?.${userProperty} }`;
280
+ }
281
+
282
+ // Handle OR conditions
283
+ const orMatch = sql.match(/\((.*?)\)\s+OR\s+\((.*?)\)/i);
284
+ if (orMatch) {
285
+ const left = convertToPrismaFilter(orMatch[1], userVar);
286
+ const right = convertToPrismaFilter(orMatch[2], userVar);
287
+
288
+ if (left !== '{}' && right !== '{}') {
289
+ return `{ OR: [${left}, ${right}] }`;
290
+ } else if (left !== '{}') {
291
+ return left;
292
+ } else if (right !== '{}') {
293
+ return right;
294
+ }
295
+ }
296
+
297
+ // Role-based checks can't be filtered in Prisma
298
+ if (sql.match(/get_current_\w+_role/i) || sql.includes('= ANY')) {
299
+ return '{}';
300
+ }
301
+
302
+ return '{}';
303
+ }
304
+
305
+ /**
306
+ * Extract Prisma filters from CASE WHEN
307
+ */
308
+ function extractPrismaFiltersFromCase(sql, userVar) {
309
+ const filters = new Set();
310
+
311
+ // Look for all field = function() patterns in the CASE statement
312
+ const fieldMatches = sql.matchAll(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/gi);
313
+
314
+ for (const match of fieldMatches) {
315
+ const field = match[1];
316
+ const funcName = match[2];
317
+
318
+ // Skip role checks
319
+ if (funcName.includes('role')) continue;
320
+
321
+ const userProperty = mapFunctionToUserProperty(funcName);
322
+ filters.add(`{ ${field}: ${userVar}?.${userProperty} }`);
323
+ }
324
+
325
+ // Look for current_setting patterns
326
+ const settingMatches = sql.matchAll(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/gi);
327
+
328
+ for (const match of settingMatches) {
329
+ const field = match[1];
330
+ const setting = match[2];
331
+ const userProperty = mapSettingToUserProperty(setting);
332
+ filters.add(`{ ${field}: ${userVar}?.${userProperty} }`);
333
+ }
334
+
335
+ const filterArray = Array.from(filters);
336
+
337
+ if (filterArray.length === 0) {
338
+ return '{}';
339
+ }
340
+
341
+ if (filterArray.length === 1) {
342
+ return filterArray[0];
343
+ }
344
+
345
+ return `{ OR: [${filterArray.join(', ')}] }`;
346
+ }
347
+
348
+ module.exports = {
349
+ convertToJavaScript,
350
+ convertToPrismaFilter,
351
+ mapFunctionToUserProperty,
352
+ mapSettingToUserProperty
353
+ };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Enhanced RLS Converter using Deep SQL Analysis
3
+ */
4
+
5
+ const DeepSQLAnalyzer = require('./deepSQLAnalyzer');
6
+ const PrismaFilterBuilder = require('./prismaFilterBuilder');
7
+
8
+ function createEnhancedConverter(functionMappings = {}, sessionVariables = {}, models = {}, relationships = {}) {
9
+ const analyzer = new DeepSQLAnalyzer();
10
+ const filterBuilder = new PrismaFilterBuilder(models, relationships);
11
+
12
+ /**
13
+ * Convert PostgreSQL RLS to JavaScript with deep analysis
14
+ */
15
+ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user', modelName = null) {
16
+ if (!sql || sql.trim() === '') {
17
+ return 'true';
18
+ }
19
+
20
+ // Use deep analysis
21
+ const analysis = analyzer.analyzeSQLForFilters(sql);
22
+ analysis.sql = sql; // Store original SQL for OR/AND detection
23
+
24
+ // If we have models and relationships, use the filter builder for better JavaScript
25
+ if (modelName && Object.keys(models).length > 0) {
26
+ return filterBuilder.buildJavaScriptCondition(modelName, analysis, dataVar, userVar);
27
+ }
28
+
29
+ // Fallback to simple JavaScript generation
30
+ const conditions = [];
31
+
32
+ // Add filter-based conditions
33
+ for (const filter of analysis.filters) {
34
+ if (filter.type.startsWith('user_') || filter.type.startsWith('session_')) {
35
+ // Dynamic user field comparison
36
+ const userField = filter.userField || filter.type.replace(/^(user_|session_)/, '');
37
+ conditions.push(`${dataVar}?.${filter.field} === ${userVar}?.${userField}`);
38
+ } else {
39
+ switch (filter.type) {
40
+ case 'equal':
41
+ if (isNaN(filter.value) && filter.value !== 'true' && filter.value !== 'false') {
42
+ conditions.push(`${dataVar}?.${filter.field} === '${filter.value}'`);
43
+ } else {
44
+ conditions.push(`${dataVar}?.${filter.field} === ${filter.value}`);
45
+ }
46
+ break;
47
+ case 'not_equal':
48
+ conditions.push(`${dataVar}?.${filter.field} !== '${filter.value}'`);
49
+ break;
50
+ case 'is_null':
51
+ conditions.push(`${dataVar}?.${filter.field} === null`);
52
+ break;
53
+ case 'not_null':
54
+ conditions.push(`${dataVar}?.${filter.field} !== null`);
55
+ break;
56
+ case 'in':
57
+ conditions.push(`[${filter.values.join(', ')}].includes(${dataVar}?.${filter.field})`);
58
+ break;
59
+ }
60
+ }
61
+ }
62
+
63
+ // Add condition-based checks (roles, etc.)
64
+ for (const condition of analysis.conditions) {
65
+ if (condition.javascript) {
66
+ // Replace user placeholder with actual variable
67
+ const jsCondition = condition.javascript.replace(/user/g, userVar);
68
+ conditions.push(jsCondition);
69
+ } else if (condition.type === 'role_any') {
70
+ conditions.push(`[${condition.roles.map(r => `'${r}'`).join(', ')}].includes(${userVar}?.role)`);
71
+ } else if (condition.type === 'role_equal') {
72
+ conditions.push(`${userVar}?.role === '${condition.role}'`);
73
+ }
74
+ }
75
+
76
+ // Handle OR/AND logic in original SQL
77
+ sql = sql.trim().replace(/\s+/g, ' ');
78
+
79
+ // Check if the entire expression is OR'd
80
+ if (sql.includes(' OR ') && !sql.includes(' AND ')) {
81
+ // If we have multiple conditions and SQL uses OR, join with ||
82
+ if (conditions.length > 1) {
83
+ return conditions.join(' || ');
84
+ }
85
+ } else if (sql.includes(' AND ') && conditions.length > 1) {
86
+ // If SQL uses AND, wrap in parentheses for clarity
87
+ return '(' + conditions.join(' && ') + ')';
88
+ }
89
+
90
+ // Default: join with AND
91
+ return conditions.length > 0 ? conditions.join(' && ') : 'true';
92
+ }
93
+
94
+ /**
95
+ * Convert to Prisma filter with deep analysis
96
+ */
97
+ function convertToPrismaFilter(sql, userVar = 'user', modelName = null) {
98
+ if (!sql || sql.trim() === '') return '{}';
99
+
100
+ // Use deep analysis
101
+ const analysis = analyzer.analyzeSQLForFilters(sql);
102
+ analysis.sql = sql; // Store original SQL for OR/AND detection
103
+
104
+ // If we have models and relationships, use the filter builder
105
+ if (modelName && Object.keys(models).length > 0) {
106
+ return filterBuilder.buildFilter(modelName, analysis, userVar);
107
+ }
108
+
109
+ // Fallback to simple filter generation
110
+ const filters = [];
111
+
112
+ for (const filter of analysis.filters) {
113
+ if (filter.type.startsWith('user_') || filter.type.startsWith('session_')) {
114
+ // Dynamic user field comparison
115
+ const userField = filter.userField || filter.type.replace(/^(user_|session_)/, '');
116
+ filters.push(`{ ${filter.field}: ${userVar}?.${userField} }`);
117
+ } else {
118
+ switch (filter.type) {
119
+ case 'equal':
120
+ if (isNaN(filter.value) && filter.value !== 'true' && filter.value !== 'false') {
121
+ filters.push(`{ ${filter.field}: '${filter.value}' }`);
122
+ } else {
123
+ filters.push(`{ ${filter.field}: ${filter.value} }`);
124
+ }
125
+ break;
126
+ case 'not_equal':
127
+ filters.push(`{ ${filter.field}: { not: '${filter.value}' } }`);
128
+ break;
129
+ case 'is_null':
130
+ filters.push(`{ ${filter.field}: null }`);
131
+ break;
132
+ case 'not_null':
133
+ filters.push(`{ ${filter.field}: { not: null } }`);
134
+ break;
135
+ case 'in':
136
+ filters.push(`{ ${filter.field}: { in: [${filter.values.join(', ')}] } }`);
137
+ break;
138
+ }
139
+ }
140
+ }
141
+
142
+ // Role checks can't be directly filtered in Prisma (they're runtime checks)
143
+ // But we can still return the data filters
144
+
145
+ if (filters.length === 0) {
146
+ return '{}';
147
+ }
148
+
149
+ if (filters.length === 1) {
150
+ return filters[0];
151
+ }
152
+
153
+ // Check if original SQL uses OR or AND
154
+ sql = sql.trim().replace(/\s+/g, ' ');
155
+
156
+ if (sql.includes(' OR ') && !sql.includes(' AND ')) {
157
+ // Use OR for multiple filters
158
+ return `{ OR: [${filters.join(', ')}] }`;
159
+ }
160
+
161
+ // Default to AND
162
+ return `{ AND: [${filters.join(', ')}] }`;
163
+ }
164
+
165
+ /**
166
+ * Analyze and get user context requirements
167
+ */
168
+ function getUserContextRequirements(sql) {
169
+ const analysis = analyzer.analyzeSQLForFilters(sql);
170
+ return analysis.userContext || {};
171
+ }
172
+
173
+ return {
174
+ convertToJavaScript,
175
+ convertToPrismaFilter,
176
+ getUserContextRequirements,
177
+ analyzer // Expose analyzer for debugging
178
+ };
179
+ }
180
+
181
+ module.exports = { createEnhancedConverter };