@rapidd/build 1.0.1 → 1.0.2
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
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
|
}
|
|
@@ -259,7 +259,7 @@ function generateFilter(policies, expressionField) {
|
|
|
259
259
|
* @param {boolean} isPostgreSQL - Whether database is PostgreSQL
|
|
260
260
|
* @param {string} userTableOption - User-specified table name
|
|
261
261
|
*/
|
|
262
|
-
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption) {
|
|
262
|
+
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, debug = false) {
|
|
263
263
|
const userTable = detectUserTable(models, userTableOption);
|
|
264
264
|
const modelNames = Object.keys(models);
|
|
265
265
|
|
|
@@ -277,11 +277,13 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
|
|
|
277
277
|
functionAnalysis = await analyzeFunctions(databaseUrl);
|
|
278
278
|
console.log(`✓ Analyzed ${Object.keys(functionAnalysis.functionMappings).length} PostgreSQL functions`);
|
|
279
279
|
|
|
280
|
-
// Save function analysis for debugging/manual adjustment
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
// Save function analysis for debugging/manual adjustment (only if --debug flag is set)
|
|
281
|
+
if (debug) {
|
|
282
|
+
const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
|
|
283
|
+
const mappingConfig = generateMappingConfig(functionAnalysis);
|
|
284
|
+
fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
|
|
285
|
+
console.log(`✓ Function mappings saved to ${configPath}`);
|
|
286
|
+
}
|
|
285
287
|
} catch (error) {
|
|
286
288
|
console.warn(`⚠ Could not analyze functions: ${error.message}`);
|
|
287
289
|
}
|
|
@@ -285,8 +285,10 @@ function buildConditionalFilter(filtersWithRoles) {
|
|
|
285
285
|
/**
|
|
286
286
|
* Generate complete rls.js file
|
|
287
287
|
*/
|
|
288
|
-
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}) {
|
|
289
|
-
|
|
288
|
+
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}, debug = false, allModels = null) {
|
|
289
|
+
// Use allModels for user table detection if provided (when filtering by model)
|
|
290
|
+
const modelsForUserDetection = allModels || models;
|
|
291
|
+
const userTable = detectUserTable(modelsForUserDetection, userTableOption);
|
|
290
292
|
const modelNames = Object.keys(models);
|
|
291
293
|
|
|
292
294
|
let policies = {};
|
|
@@ -313,11 +315,13 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
|
|
|
313
315
|
relationships
|
|
314
316
|
);
|
|
315
317
|
|
|
316
|
-
// Save function analysis for debugging
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
// Save function analysis for debugging (only if --debug flag is set)
|
|
319
|
+
if (debug) {
|
|
320
|
+
const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
|
|
321
|
+
const mappingConfig = generateMappingConfig(functionAnalysis);
|
|
322
|
+
fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
|
|
323
|
+
console.log(`✓ Function mappings saved to ${configPath}`);
|
|
324
|
+
}
|
|
321
325
|
|
|
322
326
|
// Also add user context requirements as a comment in rls.js
|
|
323
327
|
if (Object.keys(functionAnalysis.userContextRequirements).length > 0) {
|
|
@@ -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;
|