@rapidd/build 1.0.2 → 1.0.3
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/package.json +1 -1
- package/src/generators/modelGenerator.js +1 -1
- package/src/generators/rlsGenerator.js +14 -9
- package/src/generators/rlsGeneratorV2.js +24 -9
- package/src/parsers/deepSQLAnalyzer.js +22 -8
- package/src/parsers/dynamicRLSConverter.js +29 -3
- package/src/parsers/prismaParser.js +24 -2
package/package.json
CHANGED
|
@@ -150,7 +150,7 @@ function generateAllModels(models, modelDir, modelJsPath) {
|
|
|
150
150
|
|
|
151
151
|
// Copy rapidd.js to output if it exists
|
|
152
152
|
const sourceRapiddJs = path.join(process.cwd(), 'rapidd', 'rapidd.js');
|
|
153
|
-
const outputRapiddDir =
|
|
153
|
+
const outputRapiddDir = modelDir.replace(/src[\/\\]Model$/, 'rapidd');
|
|
154
154
|
const outputRapiddJs = path.join(outputRapiddDir, 'rapidd.js');
|
|
155
155
|
|
|
156
156
|
if (fs.existsSync(sourceRapiddJs)) {
|
|
@@ -34,10 +34,10 @@ function detectUserTable(models, userTableOption) {
|
|
|
34
34
|
/**
|
|
35
35
|
* Extract RLS policies from PostgreSQL
|
|
36
36
|
* @param {string} databaseUrl - PostgreSQL connection URL
|
|
37
|
-
* @param {
|
|
38
|
-
* @returns {Object} - RLS policies for each
|
|
37
|
+
* @param {Object} models - Models object with dbName mapping
|
|
38
|
+
* @returns {Object} - RLS policies for each model
|
|
39
39
|
*/
|
|
40
|
-
async function extractPostgreSQLPolicies(databaseUrl,
|
|
40
|
+
async function extractPostgreSQLPolicies(databaseUrl, models) {
|
|
41
41
|
const client = new Client({ connectionString: databaseUrl });
|
|
42
42
|
|
|
43
43
|
try {
|
|
@@ -45,8 +45,11 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
|
|
|
45
45
|
|
|
46
46
|
const policies = {};
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
|
|
48
|
+
// Create mapping from database table name to model name
|
|
49
|
+
const tableToModelMap = {};
|
|
50
|
+
for (const [modelName, modelData] of Object.entries(models)) {
|
|
51
|
+
const dbName = modelData.dbName || modelName.toLowerCase();
|
|
52
|
+
tableToModelMap[dbName] = modelName;
|
|
50
53
|
policies[modelName] = [];
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -65,11 +68,13 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
|
|
|
65
68
|
ORDER BY tablename, policyname
|
|
66
69
|
`);
|
|
67
70
|
|
|
68
|
-
// Group policies by table
|
|
71
|
+
// Group policies by model (using table to model mapping)
|
|
69
72
|
for (const row of result.rows) {
|
|
70
73
|
const tableName = row.tablename;
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const modelName = tableToModelMap[tableName];
|
|
75
|
+
|
|
76
|
+
if (modelName && policies[modelName] !== undefined) {
|
|
77
|
+
policies[modelName].push({
|
|
73
78
|
name: row.policyname,
|
|
74
79
|
permissive: row.permissive === 'PERMISSIVE',
|
|
75
80
|
roles: row.roles,
|
|
@@ -290,7 +295,7 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
|
|
|
290
295
|
|
|
291
296
|
console.log('Extracting RLS policies from database...');
|
|
292
297
|
try {
|
|
293
|
-
policies = await extractPostgreSQLPolicies(databaseUrl,
|
|
298
|
+
policies = await extractPostgreSQLPolicies(databaseUrl, models);
|
|
294
299
|
const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
|
|
295
300
|
console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
|
|
296
301
|
} catch (error) {
|
|
@@ -32,7 +32,7 @@ function detectUserTable(models, userTableOption) {
|
|
|
32
32
|
/**
|
|
33
33
|
* Extract RLS policies from PostgreSQL
|
|
34
34
|
*/
|
|
35
|
-
async function extractPostgreSQLPolicies(databaseUrl,
|
|
35
|
+
async function extractPostgreSQLPolicies(databaseUrl, models) {
|
|
36
36
|
const client = new Client({ connectionString: databaseUrl });
|
|
37
37
|
|
|
38
38
|
try {
|
|
@@ -40,8 +40,11 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
|
|
|
40
40
|
|
|
41
41
|
const policies = {};
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
|
|
43
|
+
// Create mapping from database table name to model name
|
|
44
|
+
const tableToModelMap = {};
|
|
45
|
+
for (const [modelName, modelData] of Object.entries(models)) {
|
|
46
|
+
const dbName = modelData.dbName || modelName.toLowerCase();
|
|
47
|
+
tableToModelMap[dbName] = modelName;
|
|
45
48
|
policies[modelName] = [];
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -60,11 +63,13 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
|
|
|
60
63
|
ORDER BY tablename, policyname
|
|
61
64
|
`);
|
|
62
65
|
|
|
63
|
-
// Group policies by table
|
|
66
|
+
// Group policies by model (using table to model mapping)
|
|
64
67
|
for (const row of result.rows) {
|
|
65
68
|
const tableName = row.tablename;
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
const modelName = tableToModelMap[tableName];
|
|
70
|
+
|
|
71
|
+
if (modelName && policies[modelName] !== undefined) {
|
|
72
|
+
policies[modelName].push({
|
|
68
73
|
name: row.policyname,
|
|
69
74
|
permissive: row.permissive === 'PERMISSIVE',
|
|
70
75
|
roles: row.roles,
|
|
@@ -165,9 +170,12 @@ function generateFunction(policies, expressionField, converter, modelName) {
|
|
|
165
170
|
if (expr) {
|
|
166
171
|
try {
|
|
167
172
|
const jsExpr = converter.convertToJavaScript(expr, 'data', 'user', modelName);
|
|
173
|
+
console.log(`✓ Policy '${policy.name}': ${expr.substring(0, 50)}... -> ${jsExpr.substring(0, 80)}`);
|
|
168
174
|
conditions.push(jsExpr);
|
|
169
175
|
} catch (e) {
|
|
170
|
-
|
|
176
|
+
console.warn(`⚠ Failed to convert RLS policy '${policy.name}' for ${modelName}: ${e.message}`);
|
|
177
|
+
console.warn(` SQL: ${expr}`);
|
|
178
|
+
conditions.push(`true /* TODO: Manual conversion needed for policy '${policy.name}' */`);
|
|
171
179
|
}
|
|
172
180
|
}
|
|
173
181
|
}
|
|
@@ -176,6 +184,11 @@ function generateFunction(policies, expressionField, converter, modelName) {
|
|
|
176
184
|
return 'return true;';
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
// If any condition is 'true', the entire expression is true
|
|
188
|
+
if (conditions.some(c => c === 'true' || c.startsWith('true /*'))) {
|
|
189
|
+
return 'return true;';
|
|
190
|
+
}
|
|
191
|
+
|
|
179
192
|
// Policies are OR'd together (any policy allows)
|
|
180
193
|
return `return ${conditions.join(' || ')};`;
|
|
181
194
|
}
|
|
@@ -208,7 +221,9 @@ function generateFilter(policies, expressionField, converter, modelName) {
|
|
|
208
221
|
hasDataFilter: prismaFilter !== '{}'
|
|
209
222
|
});
|
|
210
223
|
} catch (e) {
|
|
211
|
-
|
|
224
|
+
console.warn(`⚠ Failed to convert RLS filter policy '${policy.name}' for ${modelName}: ${e.message}`);
|
|
225
|
+
console.warn(` SQL: ${expr}`);
|
|
226
|
+
// On error, skip filter (fail-safe - no access)
|
|
212
227
|
}
|
|
213
228
|
}
|
|
214
229
|
}
|
|
@@ -341,7 +356,7 @@ ${Object.entries(functionAnalysis.userContextRequirements)
|
|
|
341
356
|
|
|
342
357
|
// Step 2: Extract policies
|
|
343
358
|
try {
|
|
344
|
-
policies = await extractPostgreSQLPolicies(databaseUrl,
|
|
359
|
+
policies = await extractPostgreSQLPolicies(databaseUrl, models);
|
|
345
360
|
const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
|
|
346
361
|
console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
|
|
347
362
|
} catch (error) {
|
|
@@ -180,8 +180,8 @@ class DeepSQLAnalyzer {
|
|
|
180
180
|
* Extract direct field comparisons
|
|
181
181
|
*/
|
|
182
182
|
extractDirectComparisons(sql, analysis) {
|
|
183
|
-
// Pattern: field = 'value'
|
|
184
|
-
const stringPattern = /(\w+)\s*=\s*'([^']+)'/gi;
|
|
183
|
+
// Pattern: field = 'value' (with or without quotes on field name)
|
|
184
|
+
const stringPattern = /(?:"?(\w+)"?)\s*=\s*'([^']+)'/gi;
|
|
185
185
|
let match;
|
|
186
186
|
while ((match = stringPattern.exec(sql)) !== null) {
|
|
187
187
|
const field = match[1];
|
|
@@ -200,8 +200,8 @@ class DeepSQLAnalyzer {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// Pattern: field = number
|
|
204
|
-
const numberPattern = /(\w+)\s*=\s*(\d+)(?!\s*\))/gi;
|
|
203
|
+
// Pattern: field = number (with or without quotes on field name)
|
|
204
|
+
const numberPattern = /(?:"?(\w+)"?)\s*=\s*(\d+)(?!\s*\))/gi;
|
|
205
205
|
while ((match = numberPattern.exec(sql)) !== null) {
|
|
206
206
|
const field = match[1];
|
|
207
207
|
const value = match[2];
|
|
@@ -219,8 +219,22 @@ class DeepSQLAnalyzer {
|
|
|
219
219
|
});
|
|
220
220
|
}
|
|
221
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
|
+
|
|
222
236
|
// Pattern: field IS NULL
|
|
223
|
-
const isNullPattern = /(\w+)\s+IS\s+NULL/gi;
|
|
237
|
+
const isNullPattern = /(?:"?(\w+)"?)\s+IS\s+NULL/gi;
|
|
224
238
|
while ((match = isNullPattern.exec(sql)) !== null) {
|
|
225
239
|
analysis.filters.push({
|
|
226
240
|
type: 'is_null',
|
|
@@ -244,10 +258,10 @@ class DeepSQLAnalyzer {
|
|
|
244
258
|
* Extract function-based comparisons
|
|
245
259
|
*/
|
|
246
260
|
extractFunctionComparisons(sql, analysis) {
|
|
247
|
-
// Pattern: field = function()
|
|
261
|
+
// Pattern: field = function() (with or without quotes on field name)
|
|
248
262
|
const patterns = [
|
|
249
|
-
/(\w+)\s*=\s*([\w.]+)\s*\(\s*\)/gi, // field = function()
|
|
250
|
-
/([\w.]+)\s*\(\s*\)\s*=\s*(\w+)/gi // function() = field
|
|
263
|
+
/(?:"?(\w+)"?)\s*=\s*([\w.]+)\s*\(\s*\)/gi, // field = function()
|
|
264
|
+
/([\w.]+)\s*\(\s*\)\s*=\s*(?:"?(\w+)"?)/gi // function() = field
|
|
251
265
|
];
|
|
252
266
|
|
|
253
267
|
// Normalize dots in function names for lookup
|
|
@@ -13,6 +13,23 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
|
13
13
|
|
|
14
14
|
sql = sql.trim();
|
|
15
15
|
|
|
16
|
+
// Strip outer parentheses if they wrap the entire expression
|
|
17
|
+
if (sql.startsWith('(') && sql.endsWith(')')) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let matchesOuter = true;
|
|
20
|
+
for (let i = 0; i < sql.length; i++) {
|
|
21
|
+
if (sql[i] === '(') depth++;
|
|
22
|
+
if (sql[i] === ')') depth--;
|
|
23
|
+
if (depth === 0 && i < sql.length - 1) {
|
|
24
|
+
matchesOuter = false;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (matchesOuter) {
|
|
29
|
+
sql = sql.substring(1, sql.length - 1).trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
// Normalize whitespace
|
|
17
34
|
sql = sql.replace(/\s+/g, ' ').replace(/\n/g, ' ');
|
|
18
35
|
|
|
@@ -48,8 +65,8 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
|
48
65
|
return `[${arrayValues}].includes(${userVar}?.${userProperty})`;
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
// Handle field = function() comparisons
|
|
52
|
-
const funcCompareMatch = sql.match(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/i);
|
|
68
|
+
// Handle field = function() comparisons (with or without quotes)
|
|
69
|
+
const funcCompareMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*(\w+)\s*\([^)]*\)/i);
|
|
53
70
|
if (funcCompareMatch) {
|
|
54
71
|
const field = funcCompareMatch[1];
|
|
55
72
|
const funcName = funcCompareMatch[2];
|
|
@@ -60,7 +77,7 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
// Handle field = (current_setting(...))
|
|
63
|
-
const currentSettingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
|
|
80
|
+
const currentSettingMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
|
|
64
81
|
if (currentSettingMatch) {
|
|
65
82
|
const field = currentSettingMatch[1];
|
|
66
83
|
const setting = currentSettingMatch[2];
|
|
@@ -70,6 +87,14 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
|
70
87
|
return `${dataVar}?.${field} === ${userVar}?.${userProperty}`;
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
// Handle field = literal value (true, false, numbers, strings)
|
|
91
|
+
const literalMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*(true|false|\d+|'[^']+')/i);
|
|
92
|
+
if (literalMatch) {
|
|
93
|
+
const field = literalMatch[1];
|
|
94
|
+
const value = literalMatch[2];
|
|
95
|
+
return `${dataVar}?.${field} === ${value}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
73
98
|
// Handle EXISTS subqueries
|
|
74
99
|
if (sql.includes('EXISTS')) {
|
|
75
100
|
return handleExistsSubquery(sql, dataVar, userVar);
|
|
@@ -80,6 +105,7 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
|
|
|
80
105
|
if (sql.toLowerCase() === 'false') return 'false';
|
|
81
106
|
|
|
82
107
|
// Unhandled pattern
|
|
108
|
+
console.warn(`⚠ Unhandled RLS pattern: ${sql}`);
|
|
83
109
|
return `true /* Unhandled RLS pattern: ${sql.substring(0, 60)}... */`;
|
|
84
110
|
}
|
|
85
111
|
|
|
@@ -46,6 +46,7 @@ function parsePrismaSchema(schemaPath) {
|
|
|
46
46
|
for (const { name, body } of modelBlocks) {
|
|
47
47
|
const fields = parseModelFields(body);
|
|
48
48
|
const compositeKeyFields = parseCompositeKey(body);
|
|
49
|
+
const dbName = parseMapDirective(body);
|
|
49
50
|
|
|
50
51
|
// Mark composite key fields with isId
|
|
51
52
|
if (compositeKeyFields) {
|
|
@@ -60,7 +61,8 @@ function parsePrismaSchema(schemaPath) {
|
|
|
60
61
|
name,
|
|
61
62
|
fields,
|
|
62
63
|
relations: parseModelRelations(body),
|
|
63
|
-
compositeKey: compositeKeyFields
|
|
64
|
+
compositeKey: compositeKeyFields,
|
|
65
|
+
dbName: dbName || name.toLowerCase() // Default to lowercase model name
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -94,6 +96,25 @@ function parseCompositeKey(modelBody) {
|
|
|
94
96
|
return null;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Parse @@map directive to get database table name
|
|
101
|
+
* @param {string} modelBody - The content inside model braces
|
|
102
|
+
* @returns {string|null} - Database table name, or null if no @@map directive
|
|
103
|
+
*/
|
|
104
|
+
function parseMapDirective(modelBody) {
|
|
105
|
+
const lines = modelBody.split('\n').map(line => line.trim());
|
|
106
|
+
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
// Match @@map("table_name") or @@map('table_name')
|
|
109
|
+
const match = line.match(/^@@map\(["']([^"']+)["']\)/);
|
|
110
|
+
if (match) {
|
|
111
|
+
return match[1];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
/**
|
|
98
119
|
* Parse model fields from model body
|
|
99
120
|
* @param {string} modelBody - The content inside model braces
|
|
@@ -206,7 +227,8 @@ async function parsePrismaDMMF(prismaClientPath) {
|
|
|
206
227
|
name: model.name,
|
|
207
228
|
fields: {},
|
|
208
229
|
relations: [],
|
|
209
|
-
compositeKey
|
|
230
|
+
compositeKey,
|
|
231
|
+
dbName: model.dbName || model.name.toLowerCase() // Use dbName from DMMF or default to lowercase
|
|
210
232
|
};
|
|
211
233
|
|
|
212
234
|
for (const field of model.fields) {
|