@rapidd/build 1.0.1 → 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/bin/cli.js +1 -0
- package/package.json +1 -1
- package/src/commands/build.js +90 -35
- package/src/generators/modelGenerator.js +1 -1
- package/src/generators/rlsGenerator.js +22 -15
- package/src/generators/rlsGeneratorV2.js +35 -16
- package/src/generators/routeGenerator.js +22 -28
- package/src/parsers/deepSQLAnalyzer.js +22 -8
- package/src/parsers/dynamicRLSConverter.js +29 -3
- package/src/parsers/prismaParser.js +24 -2
package/bin/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ program
|
|
|
19
19
|
.option('-m, --model <name>', 'Generate/update only specific model (e.g., "account", "user")')
|
|
20
20
|
.option('--only <component>', 'Generate only specific component: "model", "route", "rls", or "relationship"')
|
|
21
21
|
.option('--user-table <name>', 'Name of the user table for RLS (default: auto-detect from user/users)')
|
|
22
|
+
.option('--debug', 'Enable debug mode (generates rls-mappings.json)')
|
|
22
23
|
.action(async (options) => {
|
|
23
24
|
try {
|
|
24
25
|
await buildModels(options);
|
package/package.json
CHANGED
package/src/commands/build.js
CHANGED
|
@@ -12,7 +12,7 @@ const { generateAllRoutes } = require('../generators/routeGenerator');
|
|
|
12
12
|
*/
|
|
13
13
|
function generateBaseModelFile(modelJsPath) {
|
|
14
14
|
const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
|
|
15
|
-
const {ErrorResponse} = require('./Api');
|
|
15
|
+
const {ErrorResponse, getTranslation} = require('./Api');
|
|
16
16
|
|
|
17
17
|
class Model {
|
|
18
18
|
/**
|
|
@@ -50,7 +50,8 @@ class Model {
|
|
|
50
50
|
sortBy = sortBy.trim();
|
|
51
51
|
sortOrder = sortOrder.trim();
|
|
52
52
|
if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
|
|
53
|
-
|
|
53
|
+
const message = getTranslation("invalid_sort_field", {sortBy, modelName: this.constructor.name});
|
|
54
|
+
throw new ErrorResponse(message, 400);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// Query the database using Prisma with filters, pagination, and limits
|
|
@@ -71,37 +72,40 @@ class Model {
|
|
|
71
72
|
*/
|
|
72
73
|
_get = async (id, include, options = {}) =>{
|
|
73
74
|
const {omit, ..._options} = options;
|
|
74
|
-
id = Number(id)
|
|
75
|
+
id = Number(id);
|
|
75
76
|
// To determine if the record is inaccessible, either due to non-existence or insufficient permissions, two simultaneous queries are performed.
|
|
76
77
|
const _response = this.prisma.findUnique({
|
|
77
78
|
'where': {
|
|
78
|
-
'id': id
|
|
79
|
-
...this.getAccessFilter()
|
|
79
|
+
'id': id
|
|
80
80
|
},
|
|
81
81
|
'include': this.include(include),
|
|
82
82
|
'omit': {...this._omit(), ...omit},
|
|
83
83
|
..._options
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
const
|
|
86
|
+
const _checkPermission = this.prisma.findUnique({
|
|
87
87
|
'where': {
|
|
88
|
-
'id': id
|
|
88
|
+
'id': id,
|
|
89
|
+
...this.getAccessFilter()
|
|
89
90
|
},
|
|
90
91
|
'select': {
|
|
91
92
|
'id': true
|
|
92
93
|
}
|
|
93
94
|
});
|
|
94
95
|
|
|
95
|
-
const [response,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const [response, checkPermission] = await Promise.all([_response, _checkPermission]);
|
|
97
|
+
if(response){
|
|
98
|
+
if(checkPermission){
|
|
99
|
+
if(response.id != checkExistence?.id){ // IN CASE access_filter CONTAINS id FIELD
|
|
100
|
+
throw new ErrorResponse(getTranslation("no_permission"), 403);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else{
|
|
104
|
+
throw new ErrorResponse(getTranslation("no_permission"), 403);
|
|
100
105
|
}
|
|
101
|
-
throw new ErrorResponse("No permission", 403);
|
|
102
106
|
}
|
|
103
|
-
|
|
104
|
-
throw new ErrorResponse("
|
|
107
|
+
else{
|
|
108
|
+
throw new ErrorResponse(getTranslation("record_not_found"), 404);
|
|
105
109
|
}
|
|
106
110
|
return response;
|
|
107
111
|
}
|
|
@@ -276,6 +280,12 @@ class Model {
|
|
|
276
280
|
module.exports = {Model, QueryBuilder, prisma};
|
|
277
281
|
`;
|
|
278
282
|
|
|
283
|
+
// Ensure src directory exists
|
|
284
|
+
const srcDir = path.dirname(modelJsPath);
|
|
285
|
+
if (!fs.existsSync(srcDir)) {
|
|
286
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
|
|
279
289
|
fs.writeFileSync(modelJsPath, content);
|
|
280
290
|
console.log('✓ Generated src/Model.js');
|
|
281
291
|
}
|
|
@@ -359,10 +369,10 @@ async function updateRelationshipsForModel(filteredModels, relationshipsPath, pr
|
|
|
359
369
|
/**
|
|
360
370
|
* Update rls.js for a specific model
|
|
361
371
|
*/
|
|
362
|
-
async function updateRLSForModel(filteredModels, rlsPath, datasource, userTable, relationships) {
|
|
372
|
+
async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource, userTable, relationships, debug = false) {
|
|
363
373
|
const { generateRLS } = require('../generators/rlsGeneratorV2');
|
|
364
374
|
|
|
365
|
-
// Generate RLS for the filtered model
|
|
375
|
+
// Generate RLS for the filtered model (but pass all models for user table detection)
|
|
366
376
|
const tempPath = rlsPath + '.tmp';
|
|
367
377
|
await generateRLS(
|
|
368
378
|
filteredModels,
|
|
@@ -370,7 +380,9 @@ async function updateRLSForModel(filteredModels, rlsPath, datasource, userTable,
|
|
|
370
380
|
datasource.url,
|
|
371
381
|
datasource.isPostgreSQL,
|
|
372
382
|
userTable,
|
|
373
|
-
relationships
|
|
383
|
+
relationships,
|
|
384
|
+
debug,
|
|
385
|
+
allModels
|
|
374
386
|
);
|
|
375
387
|
|
|
376
388
|
// Read the generated RLS for the specific model
|
|
@@ -379,13 +391,52 @@ async function updateRLSForModel(filteredModels, rlsPath, datasource, userTable,
|
|
|
379
391
|
|
|
380
392
|
// Extract the model's RLS configuration
|
|
381
393
|
const modelName = Object.keys(filteredModels)[0];
|
|
382
|
-
const modelRlsMatch = tempContent.match(new RegExp(`${modelName}:\\s*\\{[\\s\\S]*?\\n \\}(?=,|\\n)`));
|
|
383
394
|
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
// Find the start of the model definition
|
|
396
|
+
const modelStart = tempContent.indexOf(`${modelName}:`);
|
|
397
|
+
if (modelStart === -1) {
|
|
398
|
+
throw new Error(`Could not find model ${modelName} in generated RLS`);
|
|
386
399
|
}
|
|
387
400
|
|
|
388
|
-
|
|
401
|
+
// Find the matching closing brace by counting braces
|
|
402
|
+
let braceCount = 0;
|
|
403
|
+
let inString = false;
|
|
404
|
+
let stringChar = null;
|
|
405
|
+
let i = tempContent.indexOf('{', modelStart);
|
|
406
|
+
const contentStart = i;
|
|
407
|
+
|
|
408
|
+
for (; i < tempContent.length; i++) {
|
|
409
|
+
const char = tempContent[i];
|
|
410
|
+
const prevChar = i > 0 ? tempContent[i - 1] : '';
|
|
411
|
+
|
|
412
|
+
// Handle string literals
|
|
413
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
414
|
+
if (!inString) {
|
|
415
|
+
inString = true;
|
|
416
|
+
stringChar = char;
|
|
417
|
+
} else if (char === stringChar) {
|
|
418
|
+
inString = false;
|
|
419
|
+
stringChar = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!inString) {
|
|
424
|
+
if (char === '{') braceCount++;
|
|
425
|
+
if (char === '}') braceCount--;
|
|
426
|
+
|
|
427
|
+
if (braceCount === 0) {
|
|
428
|
+
// Found the closing brace
|
|
429
|
+
const modelRls = tempContent.substring(modelStart, i + 1);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (braceCount !== 0) {
|
|
436
|
+
throw new Error(`Could not extract RLS for model ${modelName} - unmatched braces`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const modelRls = tempContent.substring(modelStart, i + 1);
|
|
389
440
|
|
|
390
441
|
// Read existing rls.js
|
|
391
442
|
if (fs.existsSync(rlsPath)) {
|
|
@@ -399,9 +450,10 @@ async function updateRLSForModel(filteredModels, rlsPath, datasource, userTable,
|
|
|
399
450
|
existingContent = existingContent.replace(existingModelPattern, modelRls);
|
|
400
451
|
} else {
|
|
401
452
|
// Add new model RLS before the closing of rls.model
|
|
453
|
+
// Find the last closing brace of a model object and add comma after it
|
|
402
454
|
existingContent = existingContent.replace(
|
|
403
|
-
/(\n}
|
|
404
|
-
|
|
455
|
+
/(\n \})\n(\};)/,
|
|
456
|
+
`$1,\n ${modelRls}\n$2`
|
|
405
457
|
);
|
|
406
458
|
}
|
|
407
459
|
|
|
@@ -415,7 +467,9 @@ async function updateRLSForModel(filteredModels, rlsPath, datasource, userTable,
|
|
|
415
467
|
datasource.url,
|
|
416
468
|
datasource.isPostgreSQL,
|
|
417
469
|
userTable,
|
|
418
|
-
relationships
|
|
470
|
+
relationships,
|
|
471
|
+
debug,
|
|
472
|
+
allModels
|
|
419
473
|
);
|
|
420
474
|
}
|
|
421
475
|
}
|
|
@@ -529,16 +583,16 @@ async function buildModels(options) {
|
|
|
529
583
|
// Generate model files
|
|
530
584
|
if (shouldGenerate.model) {
|
|
531
585
|
generateAllModels(filteredModels, modelDir, modelJsPath);
|
|
586
|
+
}
|
|
532
587
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
588
|
+
// Generate src/Model.js (base Model class) if it doesn't exist
|
|
589
|
+
if (!fs.existsSync(modelJsPath)) {
|
|
590
|
+
console.log('\nGenerating src/Model.js...');
|
|
591
|
+
generateBaseModelFile(modelJsPath);
|
|
538
592
|
}
|
|
539
593
|
|
|
540
|
-
// Generate rapidd/rapidd.js
|
|
541
|
-
if (!
|
|
594
|
+
// Generate rapidd/rapidd.js if it doesn't exist
|
|
595
|
+
if (!fs.existsSync(rapiddJsPath)) {
|
|
542
596
|
console.log('Generating rapidd/rapidd.js...');
|
|
543
597
|
generateRapiddFile(rapiddJsPath);
|
|
544
598
|
}
|
|
@@ -586,7 +640,7 @@ async function buildModels(options) {
|
|
|
586
640
|
|
|
587
641
|
if (options.model) {
|
|
588
642
|
// Update only specific model in rls.js
|
|
589
|
-
await updateRLSForModel(filteredModels, rlsPath, datasource, options.userTable, relationships);
|
|
643
|
+
await updateRLSForModel(filteredModels, models, rlsPath, datasource, options.userTable, relationships, options.debug);
|
|
590
644
|
} else {
|
|
591
645
|
// Generate RLS for all models
|
|
592
646
|
await generateRLS(
|
|
@@ -595,14 +649,15 @@ async function buildModels(options) {
|
|
|
595
649
|
datasource.url,
|
|
596
650
|
datasource.isPostgreSQL,
|
|
597
651
|
options.userTable,
|
|
598
|
-
relationships
|
|
652
|
+
relationships,
|
|
653
|
+
options.debug
|
|
599
654
|
);
|
|
600
655
|
}
|
|
601
656
|
} catch (error) {
|
|
602
657
|
console.error('Failed to generate RLS:', error.message);
|
|
603
658
|
if (!options.model) {
|
|
604
659
|
console.log('Generating permissive RLS fallback...');
|
|
605
|
-
await generateRLS(models, rlsPath, null, false, options.userTable, relationships);
|
|
660
|
+
await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
|
|
606
661
|
}
|
|
607
662
|
}
|
|
608
663
|
}
|
|
@@ -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,
|
|
@@ -259,7 +264,7 @@ function generateFilter(policies, expressionField) {
|
|
|
259
264
|
* @param {boolean} isPostgreSQL - Whether database is PostgreSQL
|
|
260
265
|
* @param {string} userTableOption - User-specified table name
|
|
261
266
|
*/
|
|
262
|
-
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption) {
|
|
267
|
+
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, debug = false) {
|
|
263
268
|
const userTable = detectUserTable(models, userTableOption);
|
|
264
269
|
const modelNames = Object.keys(models);
|
|
265
270
|
|
|
@@ -277,18 +282,20 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
|
|
|
277
282
|
functionAnalysis = await analyzeFunctions(databaseUrl);
|
|
278
283
|
console.log(`✓ Analyzed ${Object.keys(functionAnalysis.functionMappings).length} PostgreSQL functions`);
|
|
279
284
|
|
|
280
|
-
// Save function analysis for debugging/manual adjustment
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
// Save function analysis for debugging/manual adjustment (only if --debug flag is set)
|
|
286
|
+
if (debug) {
|
|
287
|
+
const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
|
|
288
|
+
const mappingConfig = generateMappingConfig(functionAnalysis);
|
|
289
|
+
fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
|
|
290
|
+
console.log(`✓ Function mappings saved to ${configPath}`);
|
|
291
|
+
}
|
|
285
292
|
} catch (error) {
|
|
286
293
|
console.warn(`⚠ Could not analyze functions: ${error.message}`);
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
console.log('Extracting RLS policies from database...');
|
|
290
297
|
try {
|
|
291
|
-
policies = await extractPostgreSQLPolicies(databaseUrl,
|
|
298
|
+
policies = await extractPostgreSQLPolicies(databaseUrl, models);
|
|
292
299
|
const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
|
|
293
300
|
console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
|
|
294
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
|
}
|
|
@@ -285,8 +300,10 @@ function buildConditionalFilter(filtersWithRoles) {
|
|
|
285
300
|
/**
|
|
286
301
|
* Generate complete rls.js file
|
|
287
302
|
*/
|
|
288
|
-
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}) {
|
|
289
|
-
|
|
303
|
+
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}, debug = false, allModels = null) {
|
|
304
|
+
// Use allModels for user table detection if provided (when filtering by model)
|
|
305
|
+
const modelsForUserDetection = allModels || models;
|
|
306
|
+
const userTable = detectUserTable(modelsForUserDetection, userTableOption);
|
|
290
307
|
const modelNames = Object.keys(models);
|
|
291
308
|
|
|
292
309
|
let policies = {};
|
|
@@ -313,11 +330,13 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
|
|
|
313
330
|
relationships
|
|
314
331
|
);
|
|
315
332
|
|
|
316
|
-
// Save function analysis for debugging
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
333
|
+
// Save function analysis for debugging (only if --debug flag is set)
|
|
334
|
+
if (debug) {
|
|
335
|
+
const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
|
|
336
|
+
const mappingConfig = generateMappingConfig(functionAnalysis);
|
|
337
|
+
fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
|
|
338
|
+
console.log(`✓ Function mappings saved to ${configPath}`);
|
|
339
|
+
}
|
|
321
340
|
|
|
322
341
|
// Also add user context requirements as a comment in rls.js
|
|
323
342
|
if (Object.keys(functionAnalysis.userContextRequirements).length > 0) {
|
|
@@ -337,7 +356,7 @@ ${Object.entries(functionAnalysis.userContextRequirements)
|
|
|
337
356
|
|
|
338
357
|
// Step 2: Extract policies
|
|
339
358
|
try {
|
|
340
|
-
policies = await extractPostgreSQLPolicies(databaseUrl,
|
|
359
|
+
policies = await extractPostgreSQLPolicies(databaseUrl, models);
|
|
341
360
|
const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
|
|
342
361
|
console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
|
|
343
362
|
} catch (error) {
|
|
@@ -7,11 +7,9 @@ const path = require('path');
|
|
|
7
7
|
* @returns {string} - Generated route code
|
|
8
8
|
*/
|
|
9
9
|
function generateRouteFile(modelName) {
|
|
10
|
-
const modelNameLower = modelName.toLowerCase();
|
|
11
10
|
const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
12
11
|
|
|
13
12
|
return `const router = require('express').Router();
|
|
14
|
-
const {Api, ErrorResponse} = require('../../../src/Api');
|
|
15
13
|
const {${className}, QueryBuilder, prisma} = require('../../../src/Model/${className}');
|
|
16
14
|
|
|
17
15
|
router.all('*', async (req, res, next) => {
|
|
@@ -20,13 +18,12 @@ router.all('*', async (req, res, next) => {
|
|
|
20
18
|
next();
|
|
21
19
|
}
|
|
22
20
|
else{
|
|
23
|
-
res.
|
|
21
|
+
return res.sendError(401, req.getTranslation("no_valid_session"));
|
|
24
22
|
}
|
|
25
23
|
});
|
|
26
24
|
|
|
27
25
|
// GET ALL
|
|
28
26
|
router.get('/', async function(req, res) {
|
|
29
|
-
let response, status_code = 200;
|
|
30
27
|
try {
|
|
31
28
|
const { q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc" } = req.query;
|
|
32
29
|
|
|
@@ -34,67 +31,64 @@ router.get('/', async function(req, res) {
|
|
|
34
31
|
const _count = req.${className}.count(q);
|
|
35
32
|
const [data, count] = await Promise.all([_data, _count]);
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
return res.sendList(data, {'take': req.${className}.take(Number(limit)), 'skip': req.${className}.skip(Number(offset)), 'total': count});
|
|
38
35
|
}
|
|
39
36
|
catch(error){
|
|
40
|
-
response = QueryBuilder.errorHandler(error);
|
|
41
|
-
|
|
37
|
+
const response = QueryBuilder.errorHandler(error);
|
|
38
|
+
return res.status(response.status_code).send(response);
|
|
42
39
|
}
|
|
43
|
-
res.status(status_code).send(response);
|
|
44
40
|
});
|
|
45
41
|
|
|
46
42
|
// GET BY ID
|
|
47
43
|
router.get('/:id', async function(req, res) {
|
|
48
|
-
let response, status_code = 200;
|
|
49
44
|
try{
|
|
50
45
|
const { include = ""} = req.query;
|
|
51
|
-
response = await req.${className}.get(req.params.id, include);
|
|
46
|
+
const response = await req.${className}.get(req.params.id, include);
|
|
47
|
+
return res.json(response);
|
|
52
48
|
}
|
|
53
49
|
catch(error){
|
|
54
|
-
response = QueryBuilder.errorHandler(error);
|
|
55
|
-
|
|
50
|
+
const response = QueryBuilder.errorHandler(error);
|
|
51
|
+
return res.status(response.status_code).send(response);
|
|
56
52
|
}
|
|
57
|
-
res.status(status_code).send(response);
|
|
58
53
|
});
|
|
59
54
|
|
|
60
55
|
// CREATE
|
|
61
56
|
router.post('/', async function(req, res) {
|
|
62
|
-
|
|
57
|
+
const payload = req.body;
|
|
63
58
|
try{
|
|
64
|
-
response = await req.${className}.create(payload);
|
|
59
|
+
const response = await req.${className}.create(payload);
|
|
60
|
+
return res.status(201).json(response);
|
|
65
61
|
}
|
|
66
62
|
catch(error){
|
|
67
|
-
response = QueryBuilder.errorHandler(error, payload);
|
|
68
|
-
|
|
63
|
+
const response = QueryBuilder.errorHandler(error, payload);
|
|
64
|
+
return res.status(response.status_code).send(response);
|
|
69
65
|
}
|
|
70
|
-
res.status(status_code).send(response);
|
|
71
66
|
});
|
|
72
67
|
|
|
73
68
|
// UPDATE
|
|
74
69
|
router.patch('/:id', async function(req, res) {
|
|
75
|
-
|
|
70
|
+
const payload = req.body;
|
|
76
71
|
try{
|
|
77
|
-
response = await req.${className}.update(req.params.id, payload);
|
|
72
|
+
const response = await req.${className}.update(req.params.id, payload);
|
|
73
|
+
return res.json(response);
|
|
78
74
|
}
|
|
79
75
|
catch(error){
|
|
80
|
-
response = QueryBuilder.errorHandler(error, payload);
|
|
81
|
-
|
|
76
|
+
const response = QueryBuilder.errorHandler(error, payload);
|
|
77
|
+
return res.status(response.status_code).send(response);
|
|
82
78
|
}
|
|
83
|
-
res.status(status_code).send(response);
|
|
84
79
|
});
|
|
85
80
|
|
|
86
81
|
// DELETE
|
|
87
82
|
router.delete('/:id', async (req, res)=>{
|
|
88
|
-
let response, status_code = 200;
|
|
89
83
|
try{
|
|
90
84
|
await req.${className}.delete(req.params.id);
|
|
91
|
-
|
|
85
|
+
const message = req.getTranslation("object_deleted_successfully", {modelName: "${className}"});
|
|
86
|
+
return res.sendResponse(200, message);
|
|
92
87
|
}
|
|
93
88
|
catch(error){
|
|
94
|
-
response = QueryBuilder.errorHandler(error);
|
|
95
|
-
|
|
89
|
+
const response = QueryBuilder.errorHandler(error);
|
|
90
|
+
return res.status(response.status_code).send(response);
|
|
96
91
|
}
|
|
97
|
-
res.status(status_code).send(response);
|
|
98
92
|
});
|
|
99
93
|
|
|
100
94
|
module.exports = router;
|
|
@@ -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) {
|