@rapidd/build 1.2.3 → 2.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/README.md +219 -68
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +31 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/src/commands/build.d.ts +17 -0
- package/dist/src/commands/build.d.ts.map +1 -0
- package/dist/src/commands/build.js +236 -0
- package/dist/src/commands/build.js.map +1 -0
- package/dist/src/generators/aclGenerator.d.ts +6 -0
- package/dist/src/generators/aclGenerator.d.ts.map +1 -0
- package/dist/src/generators/aclGenerator.js +384 -0
- package/dist/src/generators/aclGenerator.js.map +1 -0
- package/dist/src/generators/index.d.ts +4 -0
- package/dist/src/generators/index.d.ts.map +1 -0
- package/dist/src/generators/index.js +13 -0
- package/dist/src/generators/index.js.map +1 -0
- package/dist/src/generators/modelGenerator.d.ts +10 -0
- package/dist/src/generators/modelGenerator.d.ts.map +1 -0
- package/dist/src/generators/modelGenerator.js +143 -0
- package/dist/src/generators/modelGenerator.js.map +1 -0
- package/dist/src/generators/routeGenerator.d.ts +10 -0
- package/dist/src/generators/routeGenerator.d.ts.map +1 -0
- package/dist/src/generators/routeGenerator.js +172 -0
- package/dist/src/generators/routeGenerator.js.map +1 -0
- package/dist/src/parsers/datasourceParser.d.ts +11 -0
- package/dist/src/parsers/datasourceParser.d.ts.map +1 -0
- package/dist/src/parsers/datasourceParser.js +131 -0
- package/dist/src/parsers/datasourceParser.js.map +1 -0
- package/dist/src/parsers/deepSQLAnalyzer.d.ts +85 -0
- package/dist/src/parsers/deepSQLAnalyzer.d.ts.map +1 -0
- package/dist/src/parsers/deepSQLAnalyzer.js +482 -0
- package/dist/src/parsers/deepSQLAnalyzer.js.map +1 -0
- package/dist/src/parsers/enhancedRLSConverter.d.ts +14 -0
- package/dist/src/parsers/enhancedRLSConverter.d.ts.map +1 -0
- package/dist/src/parsers/enhancedRLSConverter.js +168 -0
- package/dist/src/parsers/enhancedRLSConverter.js.map +1 -0
- package/dist/src/parsers/functionAnalyzer.d.ts +55 -0
- package/dist/src/parsers/functionAnalyzer.d.ts.map +1 -0
- package/dist/src/parsers/functionAnalyzer.js +274 -0
- package/dist/src/parsers/functionAnalyzer.js.map +1 -0
- package/dist/src/parsers/index.d.ts +13 -0
- package/dist/src/parsers/index.d.ts.map +1 -0
- package/dist/src/parsers/index.js +20 -0
- package/dist/src/parsers/index.js.map +1 -0
- package/dist/src/parsers/prismaFilterBuilder.d.ts +79 -0
- package/dist/src/parsers/prismaFilterBuilder.d.ts.map +1 -0
- package/dist/src/parsers/prismaFilterBuilder.js +322 -0
- package/dist/src/parsers/prismaFilterBuilder.js.map +1 -0
- package/dist/src/parsers/prismaParser.d.ts +14 -0
- package/dist/src/parsers/prismaParser.d.ts.map +1 -0
- package/dist/src/parsers/prismaParser.js +263 -0
- package/dist/src/parsers/prismaParser.js.map +1 -0
- package/package.json +21 -13
- package/bin/cli.js +0 -33
- package/index.js +0 -11
- package/src/commands/build.js +0 -638
- package/src/generators/aclGenerator.js +0 -394
- package/src/generators/modelGenerator.js +0 -174
- package/src/generators/relationshipsGenerator.js +0 -200
- package/src/generators/routeGenerator.js +0 -119
- package/src/parsers/datasourceParser.js +0 -121
- package/src/parsers/deepSQLAnalyzer.js +0 -554
- package/src/parsers/enhancedRLSConverter.js +0 -181
- package/src/parsers/functionAnalyzer.js +0 -302
- package/src/parsers/prismaFilterBuilder.js +0 -422
- package/src/parsers/prismaParser.js +0 -287
|
@@ -1,554 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deep SQL Analyzer for PostgreSQL RLS Policies
|
|
3
|
-
* Uses extensive regex patterns to extract meaning from SQL expressions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class DeepSQLAnalyzer {
|
|
7
|
-
constructor() {
|
|
8
|
-
// Common PostgreSQL function patterns mapped to user fields
|
|
9
|
-
this.functionMappings = {
|
|
10
|
-
// User ID functions
|
|
11
|
-
'get_current_user_id': 'id',
|
|
12
|
-
'current_user_id': 'id',
|
|
13
|
-
'auth.uid': 'id',
|
|
14
|
-
'auth.user_id': 'id',
|
|
15
|
-
|
|
16
|
-
// Role functions
|
|
17
|
-
'get_current_user_role': 'role',
|
|
18
|
-
'current_user_role': 'role',
|
|
19
|
-
'current_role': 'role',
|
|
20
|
-
'auth.role': 'role',
|
|
21
|
-
|
|
22
|
-
// Tenant functions
|
|
23
|
-
'get_current_tenant_id': 'tenant_id',
|
|
24
|
-
'current_tenant_id': 'tenant_id',
|
|
25
|
-
'current_tenant': 'tenant_id',
|
|
26
|
-
|
|
27
|
-
// Organization functions
|
|
28
|
-
'get_current_org_id': 'org_id',
|
|
29
|
-
'current_org_id': 'org_id',
|
|
30
|
-
'current_organization_id': 'org_id',
|
|
31
|
-
|
|
32
|
-
// Related entity functions
|
|
33
|
-
'get_current_student_id': 'student_id',
|
|
34
|
-
'current_student_id': 'student_id',
|
|
35
|
-
'get_student_id_for_user': 'student_id',
|
|
36
|
-
|
|
37
|
-
'get_current_teacher_id': 'teacher_id',
|
|
38
|
-
'current_teacher_id': 'teacher_id',
|
|
39
|
-
'get_teacher_id_for_user': 'teacher_id',
|
|
40
|
-
|
|
41
|
-
'get_current_employee_id': 'employee_id',
|
|
42
|
-
'current_employee_id': 'employee_id',
|
|
43
|
-
'get_employee_id_for_user': 'employee_id',
|
|
44
|
-
|
|
45
|
-
'get_current_customer_id': 'customer_id',
|
|
46
|
-
'current_customer_id': 'customer_id',
|
|
47
|
-
'get_customer_id_for_user': 'customer_id'
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Session variable mappings
|
|
51
|
-
this.sessionMappings = {
|
|
52
|
-
'app.current_user_id': 'id',
|
|
53
|
-
'jwt.claims.sub': 'id',
|
|
54
|
-
'request.jwt.claims.sub': 'id',
|
|
55
|
-
'request.jwt.claim.sub': 'id',
|
|
56
|
-
|
|
57
|
-
'app.current_role': 'role',
|
|
58
|
-
'jwt.claims.role': 'role',
|
|
59
|
-
'request.jwt.claim.role': 'role',
|
|
60
|
-
|
|
61
|
-
'app.current_tenant': 'tenant_id',
|
|
62
|
-
'app.tenant_id': 'tenant_id',
|
|
63
|
-
'jwt.claims.tenant_id': 'tenant_id',
|
|
64
|
-
|
|
65
|
-
'app.org_id': 'org_id',
|
|
66
|
-
'app.organization_id': 'org_id'
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Analyze SQL expression and extract Prisma filters
|
|
72
|
-
*/
|
|
73
|
-
analyzeSQLForFilters(sql) {
|
|
74
|
-
if (!sql || sql.trim() === '') {
|
|
75
|
-
return { filters: [], conditions: [], userContext: {} };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const analysis = {
|
|
79
|
-
filters: [],
|
|
80
|
-
conditions: [],
|
|
81
|
-
userContext: {}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Normalize SQL
|
|
85
|
-
sql = this.normalizeSql(sql);
|
|
86
|
-
|
|
87
|
-
// Remove EXISTS subqueries with proper parentheses matching
|
|
88
|
-
const sqlWithoutExists = this.removeExistsSubqueries(sql);
|
|
89
|
-
|
|
90
|
-
// Extract different types of conditions (use sqlWithoutExists to avoid EXISTS internals)
|
|
91
|
-
this.extractDirectComparisons(sqlWithoutExists, analysis);
|
|
92
|
-
this.extractFunctionComparisons(sqlWithoutExists, analysis);
|
|
93
|
-
this.extractSessionVariableComparisons(sqlWithoutExists, analysis);
|
|
94
|
-
this.extractInClauses(sqlWithoutExists, analysis);
|
|
95
|
-
|
|
96
|
-
// Extract EXISTS from original SQL
|
|
97
|
-
this.extractExistsSubqueries(sql, analysis);
|
|
98
|
-
|
|
99
|
-
// Extract from original SQL for these
|
|
100
|
-
this.extractCaseWhenConditions(sql, analysis);
|
|
101
|
-
this.extractRoleChecks(sql, analysis);
|
|
102
|
-
this.extractComplexJoins(sql, analysis);
|
|
103
|
-
|
|
104
|
-
return analysis;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Normalize SQL for easier parsing
|
|
109
|
-
*/
|
|
110
|
-
normalizeSql(sql) {
|
|
111
|
-
let normalized = sql
|
|
112
|
-
.replace(/\s+/g, ' ')
|
|
113
|
-
.replace(/\n/g, ' ')
|
|
114
|
-
.replace(/\t/g, ' ')
|
|
115
|
-
.replace(/::character varying/gi, '') // Remove multi-word type casts first
|
|
116
|
-
.replace(/::[\w_]+\s*\[\]/g, ']') // Replace ::type[] with just ] (preserve array bracket)
|
|
117
|
-
.replace(/::[\w_]+/g, '') // Remove simple type casts
|
|
118
|
-
.trim();
|
|
119
|
-
|
|
120
|
-
// Remove balanced outer wrapping parentheses
|
|
121
|
-
while (normalized.startsWith('(') && normalized.endsWith(')')) {
|
|
122
|
-
const inner = normalized.slice(1, -1);
|
|
123
|
-
// Count parentheses to check if outer pair is balanced
|
|
124
|
-
let depth = 0;
|
|
125
|
-
let isBalanced = true;
|
|
126
|
-
for (let i = 0; i < inner.length; i++) {
|
|
127
|
-
if (inner[i] === '(') depth++;
|
|
128
|
-
if (inner[i] === ')') depth--;
|
|
129
|
-
// If depth goes negative, the outer parens are needed
|
|
130
|
-
if (depth < 0) {
|
|
131
|
-
isBalanced = false;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// Only remove if balanced and not a single function call
|
|
136
|
-
if (isBalanced && depth === 0 && (inner.includes(' = ') || inner.includes(' AND ') || inner.includes(' OR '))) {
|
|
137
|
-
normalized = inner.trim();
|
|
138
|
-
} else {
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return normalized;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Remove EXISTS subqueries with proper parentheses matching
|
|
148
|
-
*/
|
|
149
|
-
removeExistsSubqueries(sql) {
|
|
150
|
-
let result = sql;
|
|
151
|
-
let changed = true;
|
|
152
|
-
|
|
153
|
-
// Keep removing EXISTS clauses until none are left
|
|
154
|
-
while (changed) {
|
|
155
|
-
changed = false;
|
|
156
|
-
const existsIndex = result.search(/EXISTS\s*\(/i);
|
|
157
|
-
|
|
158
|
-
if (existsIndex !== -1) {
|
|
159
|
-
// Find the matching closing parenthesis
|
|
160
|
-
const startParen = result.indexOf('(', existsIndex);
|
|
161
|
-
let depth = 1;
|
|
162
|
-
let endParen = startParen + 1;
|
|
163
|
-
|
|
164
|
-
while (endParen < result.length && depth > 0) {
|
|
165
|
-
if (result[endParen] === '(') depth++;
|
|
166
|
-
if (result[endParen] === ')') depth--;
|
|
167
|
-
endParen++;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Replace EXISTS(...) with 'true'
|
|
171
|
-
result = result.substring(0, existsIndex) + 'true' + result.substring(endParen);
|
|
172
|
-
changed = true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return result;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Extract direct field comparisons
|
|
181
|
-
*/
|
|
182
|
-
extractDirectComparisons(sql, analysis) {
|
|
183
|
-
// Pattern: field = 'value' (with or without quotes on field name)
|
|
184
|
-
const stringPattern = /(?:"?(\w+)"?)\s*=\s*'([^']+)'/gi;
|
|
185
|
-
let match;
|
|
186
|
-
while ((match = stringPattern.exec(sql)) !== null) {
|
|
187
|
-
const field = match[1];
|
|
188
|
-
const value = match[2];
|
|
189
|
-
|
|
190
|
-
// Skip if field is actually a function
|
|
191
|
-
if (field.toLowerCase().includes('current') || field.toLowerCase().includes('get')) {
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
analysis.filters.push({
|
|
196
|
-
type: 'equal',
|
|
197
|
-
field: field,
|
|
198
|
-
value: value,
|
|
199
|
-
prisma: `{ ${field}: '${value}' }`
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Pattern: field = number (with or without quotes on field name)
|
|
204
|
-
const numberPattern = /(?:"?(\w+)"?)\s*=\s*(\d+)(?!\s*\))/gi;
|
|
205
|
-
while ((match = numberPattern.exec(sql)) !== null) {
|
|
206
|
-
const field = match[1];
|
|
207
|
-
const value = match[2];
|
|
208
|
-
|
|
209
|
-
// Skip if field is actually a function
|
|
210
|
-
if (field.toLowerCase().includes('current') || field.toLowerCase().includes('get')) {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
analysis.filters.push({
|
|
215
|
-
type: 'equal',
|
|
216
|
-
field: field,
|
|
217
|
-
value: value,
|
|
218
|
-
prisma: `{ ${field}: ${value} }`
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Pattern: field = true/false (with or without quotes on field name)
|
|
223
|
-
const booleanPattern = /(?:"?(\w+)"?)\s*=\s*(true|false)/gi;
|
|
224
|
-
while ((match = booleanPattern.exec(sql)) !== null) {
|
|
225
|
-
const field = match[1];
|
|
226
|
-
const value = match[2].toLowerCase();
|
|
227
|
-
|
|
228
|
-
analysis.filters.push({
|
|
229
|
-
type: 'equal',
|
|
230
|
-
field: field,
|
|
231
|
-
value: value,
|
|
232
|
-
prisma: `{ ${field}: ${value} }`
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Pattern: field IS NULL
|
|
237
|
-
const isNullPattern = /(?:"?(\w+)"?)\s+IS\s+NULL/gi;
|
|
238
|
-
while ((match = isNullPattern.exec(sql)) !== null) {
|
|
239
|
-
analysis.filters.push({
|
|
240
|
-
type: 'is_null',
|
|
241
|
-
field: match[1],
|
|
242
|
-
prisma: `{ ${match[1]}: null }`
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Pattern: field IS NOT NULL
|
|
247
|
-
const isNotNullPattern = /(\w+)\s+IS\s+NOT\s+NULL/gi;
|
|
248
|
-
while ((match = isNotNullPattern.exec(sql)) !== null) {
|
|
249
|
-
analysis.filters.push({
|
|
250
|
-
type: 'not_null',
|
|
251
|
-
field: match[1],
|
|
252
|
-
prisma: `{ ${match[1]}: { not: null } }`
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Extract function-based comparisons
|
|
259
|
-
*/
|
|
260
|
-
extractFunctionComparisons(sql, analysis) {
|
|
261
|
-
// Pattern: field = function() (with or without quotes on field name)
|
|
262
|
-
const patterns = [
|
|
263
|
-
/(?:"?(\w+)"?)\s*=\s*([\w.]+)\s*\(\s*\)/gi, // field = function()
|
|
264
|
-
/([\w.]+)\s*\(\s*\)\s*=\s*(?:"?(\w+)"?)/gi // function() = field
|
|
265
|
-
];
|
|
266
|
-
|
|
267
|
-
// Normalize dots in function names for lookup
|
|
268
|
-
const normalizeFuncName = (name) => name.replace(/\./g, '_');
|
|
269
|
-
|
|
270
|
-
for (let i = 0; i < patterns.length; i++) {
|
|
271
|
-
const pattern = patterns[i];
|
|
272
|
-
let match;
|
|
273
|
-
while ((match = pattern.exec(sql)) !== null) {
|
|
274
|
-
let field, funcName;
|
|
275
|
-
|
|
276
|
-
// First pattern is: field = function()
|
|
277
|
-
// Second pattern is: function() = field
|
|
278
|
-
if (i === 0) {
|
|
279
|
-
field = match[1];
|
|
280
|
-
funcName = normalizeFuncName(match[2]);
|
|
281
|
-
} else {
|
|
282
|
-
funcName = normalizeFuncName(match[1]);
|
|
283
|
-
field = match[2];
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Look up function mapping (check both with/without underscore and original)
|
|
287
|
-
let userField = this.functionMappings[funcName] || this.functionMappings[funcName.toLowerCase()];
|
|
288
|
-
|
|
289
|
-
// Also try the original name without normalization
|
|
290
|
-
if (!userField) {
|
|
291
|
-
const originalName = (i === 0 ? match[2] : match[1]);
|
|
292
|
-
userField = this.functionMappings[originalName] || this.functionMappings[originalName.toLowerCase()];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (userField) {
|
|
296
|
-
// Skip if field is actually a function (e.g., both sides are functions)
|
|
297
|
-
if (field.toLowerCase().includes('current') || field.toLowerCase().includes('get') || field.includes('(')) {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Skip if this is part of an ANY clause (handled by extractRoleChecks)
|
|
302
|
-
if (sql.includes(match[0] + ' = ANY') || sql.includes(match[0] + '= ANY') ||
|
|
303
|
-
sql.includes(match[0] + ' =ANY') || sql.includes(match[0] + '=ANY')) {
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
analysis.filters.push({
|
|
308
|
-
type: `user_${userField}`,
|
|
309
|
-
field: field,
|
|
310
|
-
userField: userField,
|
|
311
|
-
prisma: `{ ${field}: user?.${userField} }`
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Track user context requirements
|
|
315
|
-
const contextKey = `requires${userField.charAt(0).toUpperCase()}${userField.slice(1).replace(/_(.)/g, (_, c) => c.toUpperCase())}`;
|
|
316
|
-
analysis.userContext[contextKey] = true;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Pattern: function() = 'value'
|
|
322
|
-
const funcValuePattern = /([\w.]+)\s*\(\s*\)\s*=\s*'([^']+)'/gi;
|
|
323
|
-
let match;
|
|
324
|
-
while ((match = funcValuePattern.exec(sql)) !== null) {
|
|
325
|
-
const funcName = match[1].replace(/\./g, '_');
|
|
326
|
-
const value = match[2];
|
|
327
|
-
|
|
328
|
-
const userField = this.functionMappings[funcName] || this.functionMappings[funcName.toLowerCase()];
|
|
329
|
-
|
|
330
|
-
if (userField === 'role') {
|
|
331
|
-
analysis.conditions.push({
|
|
332
|
-
type: 'role_equal',
|
|
333
|
-
role: value,
|
|
334
|
-
javascript: `user?.role === '${value}'`
|
|
335
|
-
});
|
|
336
|
-
analysis.userContext.requiresRole = true;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Extract session variable comparisons
|
|
343
|
-
*/
|
|
344
|
-
extractSessionVariableComparisons(sql, analysis) {
|
|
345
|
-
// Pattern: field = current_setting('...')
|
|
346
|
-
const patterns = [
|
|
347
|
-
/(\w+)\s*=\s*(?:\(?\s*current_setting\s*\(\s*'([^']+)'[^)]*\)\s*\)?)/gi,
|
|
348
|
-
/current_setting\s*\(\s*'([^']+)'[^)]*\)\s*=\s*(\w+)/gi
|
|
349
|
-
];
|
|
350
|
-
|
|
351
|
-
for (let i = 0; i < patterns.length; i++) {
|
|
352
|
-
const pattern = patterns[i];
|
|
353
|
-
let match;
|
|
354
|
-
while ((match = pattern.exec(sql)) !== null) {
|
|
355
|
-
let field, setting;
|
|
356
|
-
|
|
357
|
-
// First pattern: field = current_setting(...)
|
|
358
|
-
// Second pattern: current_setting(...) = field
|
|
359
|
-
if (i === 0) {
|
|
360
|
-
field = match[1];
|
|
361
|
-
setting = match[2];
|
|
362
|
-
} else {
|
|
363
|
-
setting = match[1];
|
|
364
|
-
field = match[2];
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const userField = this.sessionMappings[setting];
|
|
368
|
-
|
|
369
|
-
if (userField) {
|
|
370
|
-
// Skip if field is actually a function
|
|
371
|
-
if (field.includes('(')) continue;
|
|
372
|
-
|
|
373
|
-
analysis.filters.push({
|
|
374
|
-
type: `session_${userField}`,
|
|
375
|
-
field: field,
|
|
376
|
-
userField: userField,
|
|
377
|
-
prisma: `{ ${field}: user?.${userField} }`
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// Track user context requirements
|
|
381
|
-
const contextKey = `requires${userField.charAt(0).toUpperCase()}${userField.slice(1).replace(/_(.)/g, (_, c) => c.toUpperCase())}`;
|
|
382
|
-
analysis.userContext[contextKey] = true;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Extract IN clauses
|
|
390
|
-
*/
|
|
391
|
-
extractInClauses(sql, analysis) {
|
|
392
|
-
// Pattern: field IN (values)
|
|
393
|
-
const inPattern = /(\w+)\s+IN\s*\(([^)]+)\)/gi;
|
|
394
|
-
let match;
|
|
395
|
-
|
|
396
|
-
while ((match = inPattern.exec(sql)) !== null) {
|
|
397
|
-
const field = match[1];
|
|
398
|
-
const values = match[2];
|
|
399
|
-
|
|
400
|
-
// Skip if field is a function
|
|
401
|
-
if (field.toLowerCase().includes('current') || field.toLowerCase().includes('get')) {
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Check if it's a subquery
|
|
406
|
-
if (values.toLowerCase().includes('select')) {
|
|
407
|
-
// Handle subquery
|
|
408
|
-
analysis.conditions.push({
|
|
409
|
-
type: 'in_subquery',
|
|
410
|
-
field: field,
|
|
411
|
-
subquery: values,
|
|
412
|
-
prisma: `/* IN subquery for ${field} - requires manual implementation */`
|
|
413
|
-
});
|
|
414
|
-
} else {
|
|
415
|
-
// Parse values
|
|
416
|
-
const valueList = values
|
|
417
|
-
.split(',')
|
|
418
|
-
.map(v => v.trim().replace(/'/g, ''));
|
|
419
|
-
|
|
420
|
-
const quotedValues = valueList.map(v =>
|
|
421
|
-
isNaN(v) && v !== 'true' && v !== 'false' ? `'${v}'` : v
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
analysis.filters.push({
|
|
425
|
-
type: 'in',
|
|
426
|
-
field: field,
|
|
427
|
-
values: quotedValues,
|
|
428
|
-
prisma: `{ ${field}: { in: [${quotedValues.join(', ')}] } }`
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Extract EXISTS subqueries
|
|
436
|
-
*/
|
|
437
|
-
extractExistsSubqueries(sql, analysis) {
|
|
438
|
-
const existsPattern = /EXISTS\s*\(([^)]+(?:\([^)]*\)[^)]*)*)\)/gi;
|
|
439
|
-
let match;
|
|
440
|
-
|
|
441
|
-
while ((match = existsPattern.exec(sql)) !== null) {
|
|
442
|
-
const subquery = match[1];
|
|
443
|
-
|
|
444
|
-
// Try to extract the table and join condition
|
|
445
|
-
const fromMatch = subquery.match(/FROM\s+(\w+)/i);
|
|
446
|
-
const whereMatch = subquery.match(/WHERE\s+(.+)/i);
|
|
447
|
-
|
|
448
|
-
if (fromMatch) {
|
|
449
|
-
const table = fromMatch[1];
|
|
450
|
-
const condition = whereMatch ? whereMatch[1] : '';
|
|
451
|
-
|
|
452
|
-
analysis.conditions.push({
|
|
453
|
-
type: 'exists',
|
|
454
|
-
table: table,
|
|
455
|
-
condition: condition,
|
|
456
|
-
prisma: `/* EXISTS check on ${table} - implement as relation check */`
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Extract CASE WHEN conditions
|
|
464
|
-
*/
|
|
465
|
-
extractCaseWhenConditions(sql, analysis) {
|
|
466
|
-
const casePattern = /CASE\s+WHEN\s+([^THEN]+)\s+THEN\s+([^WHEN|ELSE|END]+)(?:\s+WHEN\s+([^THEN]+)\s+THEN\s+([^WHEN|ELSE|END]+))*(?:\s+ELSE\s+([^END]+))?\s+END/gi;
|
|
467
|
-
let match;
|
|
468
|
-
|
|
469
|
-
while ((match = casePattern.exec(sql)) !== null) {
|
|
470
|
-
const conditions = [];
|
|
471
|
-
|
|
472
|
-
// Extract WHEN conditions
|
|
473
|
-
const whenPattern = /WHEN\s+([^THEN]+)\s+THEN\s+([^WHEN|ELSE|END]+)/gi;
|
|
474
|
-
let whenMatch;
|
|
475
|
-
|
|
476
|
-
while ((whenMatch = whenPattern.exec(match[0])) !== null) {
|
|
477
|
-
const condition = whenMatch[1].trim();
|
|
478
|
-
const result = whenMatch[2].trim();
|
|
479
|
-
|
|
480
|
-
conditions.push({
|
|
481
|
-
condition: condition,
|
|
482
|
-
result: result
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (conditions.length > 0) {
|
|
487
|
-
analysis.conditions.push({
|
|
488
|
-
type: 'case_when',
|
|
489
|
-
conditions: conditions,
|
|
490
|
-
prisma: `/* CASE WHEN logic - requires conditional implementation */`
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Extract role-based checks
|
|
498
|
-
*/
|
|
499
|
-
extractRoleChecks(sql, analysis) {
|
|
500
|
-
// Pattern: (function()) = ANY((ARRAY[...])) or function() = ANY(ARRAY[...])
|
|
501
|
-
// Handle optional wrapping parens around function and multiple parens around ARRAY
|
|
502
|
-
const anyArrayPattern = /\(?([\w.]+)\s*\(\s*\)\)?\s*=\s*ANY\s*\(+\s*(?:ARRAY\s*)?\[([^\]]+)\]/gi;
|
|
503
|
-
let match;
|
|
504
|
-
|
|
505
|
-
while ((match = anyArrayPattern.exec(sql)) !== null) {
|
|
506
|
-
const funcName = match[1].replace(/\./g, '_');
|
|
507
|
-
const values = match[2];
|
|
508
|
-
|
|
509
|
-
const userField = this.functionMappings[funcName] || this.functionMappings[funcName.toLowerCase()];
|
|
510
|
-
|
|
511
|
-
if (userField === 'role') {
|
|
512
|
-
const roles = values
|
|
513
|
-
.split(',')
|
|
514
|
-
.map(r => r.trim().replace(/'/g, ''));
|
|
515
|
-
|
|
516
|
-
analysis.conditions.push({
|
|
517
|
-
type: 'role_any',
|
|
518
|
-
roles: roles,
|
|
519
|
-
javascript: `[${roles.map(r => `'${r}'`).join(', ')}].includes(user?.role)`
|
|
520
|
-
});
|
|
521
|
-
analysis.userContext.requiresRole = true;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Extract complex JOIN conditions
|
|
528
|
-
*/
|
|
529
|
-
extractComplexJoins(sql, analysis) {
|
|
530
|
-
// Look for patterns that suggest relationships
|
|
531
|
-
const relationPatterns = [
|
|
532
|
-
// user owns resource through intermediate table
|
|
533
|
-
/(\w+)\s+IN\s*\(\s*SELECT\s+\w+\s+FROM\s+(\w+)\s+WHERE\s+(\w+)=/gi,
|
|
534
|
-
// team/group membership
|
|
535
|
-
/(\w+)\.(\w+)\s+IN\s*\(\s*SELECT\s+\w+\s+FROM\s+(\w+_members?)\s+WHERE/gi
|
|
536
|
-
];
|
|
537
|
-
|
|
538
|
-
for (const pattern of relationPatterns) {
|
|
539
|
-
let match;
|
|
540
|
-
while ((match = pattern.exec(sql)) !== null) {
|
|
541
|
-
if (match[2]) {
|
|
542
|
-
analysis.conditions.push({
|
|
543
|
-
type: 'relation',
|
|
544
|
-
table: match[2],
|
|
545
|
-
field: match[1],
|
|
546
|
-
prisma: `/* Relation check through ${match[2]} table */`
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
module.exports = DeepSQLAnalyzer;
|