@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidd/build",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Dynamic code generator that transforms Prisma schemas into Express.js CRUD APIs with PostgreSQL RLS-to-JavaScript translation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
- throw new ErrorResponse(\`Parameter sortBy '\${sortBy}' is not a valid field of \${this.constructor.name}\`, 400);
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 _checkExistence = this.prisma.findUnique({
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, checkExistence] = await Promise.all([_response, _checkExistence]);
96
-
97
- if(response == null){
98
- if(checkExistence == null){
99
- throw new ErrorResponse("Record not found", 404);
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
- if(response.id != checkExistence?.id){ // IN CASE access_filter CONTAINS id FIELD
104
- throw new ErrorResponse("No permission", 403);
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
- if (!modelRlsMatch) {
385
- throw new Error(`Could not extract RLS for model ${modelName}`);
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
- const modelRls = modelRlsMatch[0];
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};[\s]*\n[\s]*module\.exports)/,
404
- `,\n ${modelRls}\n};$1`
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
- // Generate src/Model.js (base Model class) - only if not filtering by model
534
- if (!options.model) {
535
- console.log('\nGenerating src/Model.js...');
536
- generateBaseModelFile(modelJsPath);
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 - only if not filtering by model
541
- if (!options.model && !options.only) {
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
- const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
282
- const mappingConfig = generateMappingConfig(functionAnalysis);
283
- fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
284
- console.log(`✓ Function mappings saved to ${configPath}`);
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
- const userTable = detectUserTable(models, userTableOption);
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
- const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
318
- const mappingConfig = generateMappingConfig(functionAnalysis);
319
- fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
320
- console.log(`✓ Function mappings saved to ${configPath}`);
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.status(401).send({'status_code': res.statusCode, 'message': "No valid session"});
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
- response = Api.getListResponseBody(data, {'take': req.${className}.take(Number(limit)), 'skip': req.${className}.skip(Number(offset)), 'total': count});
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
- status_code = response.status_code;
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
- status_code = response.status_code;
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
- let response, status_code = 201, payload = req.body;
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
- status_code = response.status_code;
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
- let response, status_code = 200, payload = req.body;
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
- status_code = response.status_code;
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
- response = {'status_code': status_code, 'message': "${className} successfully deleted"}
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
- status_code = response.status_code;
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;