@rapidd/build 1.0.7 → 1.0.8

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
@@ -17,9 +17,9 @@ program
17
17
  .option('-s, --schema <path>', 'Path to Prisma schema file', process.env.PRISMA_SCHEMA_PATH || './prisma/schema.prisma')
18
18
  .option('-o, --output <path>', 'Output base directory', './')
19
19
  .option('-m, --model <name>', 'Generate/update only specific model (e.g., "account", "user")')
20
- .option('--only <component>', 'Generate only specific component: "model", "route", "rls", or "relationship"')
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)')
20
+ .option('--only <component>', 'Generate only specific component: "model", "route", "acl", or "relationship"')
21
+ .option('--user-table <name>', 'Name of the user table for ACL (default: auto-detect from user/users)')
22
+ .option('--debug', 'Enable debug mode (generates acl-mappings.json)')
23
23
  .action(async (options) => {
24
24
  try {
25
25
  await buildModels(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidd/build",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
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": {
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const { parsePrismaSchema, parsePrismaDMMF } = require('../parsers/prismaParser');
4
4
  const { generateAllModels } = require('../generators/modelGenerator');
5
5
  const { generateRelationshipsFromDMMF, generateRelationshipsFromSchema } = require('../generators/relationshipsGenerator');
6
- const { generateRLS } = require('../generators/rlsGeneratorV2');
6
+ const { generateACL } = require('../generators/aclGenerator');
7
7
  const { parseDatasource } = require('../parsers/datasourceParser');
8
8
  const { generateAllRoutes } = require('../generators/routeGenerator');
9
9
 
@@ -11,8 +11,8 @@ const { generateAllRoutes } = require('../generators/routeGenerator');
11
11
  * Generate src/Model.js base class file
12
12
  */
13
13
  function generateBaseModelFile(modelJsPath) {
14
- const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
15
- const {rls} = require('../rapidd/rapidd');
14
+ const content = `const { QueryBuilder, prisma, prismaTransaction } = require("./QueryBuilder");
15
+ const {acl} = require('../rapidd/rapidd');
16
16
  const {ErrorResponse} = require('./Api');
17
17
 
18
18
  class Model {
@@ -23,7 +23,7 @@ class Model {
23
23
  constructor(name, options){
24
24
  this.modelName = name;
25
25
  this.queryBuilder = new QueryBuilder(name);
26
- this.rls = rls.model[name] || {};
26
+ this.acl = acl.model[name] || {};
27
27
  this.options = options || {}
28
28
  this.user = this.options.user || {'id': 1, 'role': 'application'};
29
29
  this.user_id = this.user ? this.user.id : null;
@@ -32,12 +32,11 @@ class Model {
32
32
  _select = (fields) => this.queryBuilder.select(fields);
33
33
  _filter = (q) => this.queryBuilder.filter(q);
34
34
  _include = (include) => this.queryBuilder.include(include, this.user);
35
- // RLS METHODS
36
- _canCreate = () => this.rls.canCreate(this.user);
37
- _hasAccess = (data) => this.rls.hasAccess?.(data, this.user) || false;
38
- _getAccessFilter = () => this.rls.getAccessFilter?.(this.user);
39
- _getUpdateFilter = () => this.rls.getUpdateFilter(this.user);
40
- _getDeleteFilter = () => this.rls.getDeleteFilter(this.user);
35
+ // ACL METHODS
36
+ _canCreate = () => this.acl.canCreate(this.user);
37
+ _getAccessFilter = () => this.acl.getAccessFilter?.(this.user);
38
+ _getUpdateFilter = () => this.acl.getUpdateFilter(this.user);
39
+ _getDeleteFilter = () => this.acl.getDeleteFilter(this.user);
41
40
  _omit = () => this.queryBuilder.omit(this.user);
42
41
 
43
42
  /**
@@ -61,15 +60,21 @@ class Model {
61
60
  }
62
61
 
63
62
  // Query the database using Prisma with filters, pagination, and limits
64
- return await this.prisma.findMany({
65
- 'where': this.filter(q),
66
- 'include': this.include(include),
67
- 'take': take,
68
- 'skip': skip,
69
- 'orderBy': this.sort(sortBy, sortOrder),
70
- 'omit': this._omit(),
71
- ...options
72
- });
63
+ const [data, total] = await prismaTransaction([
64
+ (tx) => tx[this.name].findMany({
65
+ 'where': this.filter(q),
66
+ 'include': this.include(include),
67
+ 'take': take,
68
+ 'skip': skip,
69
+ 'orderBy': this.sort(sortBy, sortOrder),
70
+ 'omit': this._omit(),
71
+ ...options
72
+ }),
73
+ (tx) => tx[this.name].count({
74
+ 'where': this.filter(q)
75
+ })
76
+ ]);
77
+ return {data, total};
73
78
  }
74
79
  /**
75
80
  * @param {number} id
@@ -288,15 +293,6 @@ class Model {
288
293
  return this._getAccessFilter();
289
294
  }
290
295
 
291
- /**
292
- *
293
- * @param {*} data
294
- * @returns {boolean}
295
- */
296
- hasAccess(data) {
297
- return this.user.role == "application" ? true : this._hasAccess(data, this.user);
298
- }
299
-
300
296
  /**
301
297
  * Check if user can create records
302
298
  * @returns {boolean}
@@ -307,7 +303,7 @@ class Model {
307
303
  }
308
304
 
309
305
  /**
310
- * Get update filter for RLS
306
+ * Get update filter for ACL
311
307
  * @returns {Object|false}
312
308
  */
313
309
  getUpdateFilter(){
@@ -319,7 +315,7 @@ class Model {
319
315
  }
320
316
 
321
317
  /**
322
- * Get delete filter for RLS
318
+ * Get delete filter for ACL
323
319
  * @returns {Object|false}
324
320
  */
325
321
  getDeleteFilter(){
@@ -355,11 +351,17 @@ module.exports = {Model, QueryBuilder, prisma};
355
351
 
356
352
  /**
357
353
  * Generate rapidd/rapidd.js file
354
+ * @param {string} rapiddJsPath - Path to rapidd.js
355
+ * @param {boolean} isPostgreSQL - Whether the database is PostgreSQL
358
356
  */
359
- function generateRapiddFile(rapiddJsPath) {
360
- const content = `const { PrismaClient } = require('../prisma/client');
357
+ function generateRapiddFile(rapiddJsPath, isPostgreSQL = true) {
358
+ let content;
359
+
360
+ if (isPostgreSQL) {
361
+ // PostgreSQL version with RLS support
362
+ content = `const { PrismaClient } = require('../prisma/client');
361
363
  const { AsyncLocalStorage } = require('async_hooks');
362
- const rls = require('./rls');
364
+ const acl = require('./acl');
363
365
 
364
366
  // Request Context Storage
365
367
  const requestContext = new AsyncLocalStorage();
@@ -433,6 +435,20 @@ const prisma = basePrisma.$extends({
433
435
  },
434
436
  });
435
437
 
438
+ // Helper for batch operations in single transaction
439
+ async function prismaTransaction(operations) {
440
+ const context = requestContext.getStore();
441
+
442
+ if (!context?.userId || !context?.userRole) {
443
+ return Promise.all(operations);
444
+ }
445
+
446
+ return basePrisma.$transaction(async (tx) => {
447
+ await setRLSVariables(tx, context.userId, context.userRole);
448
+ return Promise.all(operations.map(op => op(tx)));
449
+ });
450
+ }
451
+
436
452
  // Alternative approach: Manual transaction wrapper
437
453
  class PrismaWithRLS {
438
454
  constructor() {
@@ -557,6 +573,7 @@ app.get('/api/users', authenticateUser, setRLSContext, async (req, res) => {
557
573
 
558
574
  module.exports = {
559
575
  prisma,
576
+ prismaTransaction,
560
577
  basePrisma, // Export base for auth operations that don't need RLS
561
578
  PrismaClient,
562
579
  requestContext,
@@ -567,9 +584,26 @@ module.exports = {
567
584
  prismaWithRLS,
568
585
  getRLSConfig,
569
586
  setRLSVariables,
570
- rls
587
+ acl
571
588
  };
572
589
  `;
590
+ } else {
591
+ // Non-PostgreSQL version (MySQL, SQLite, etc.) - simplified without RLS
592
+ content = `const { PrismaClient } = require('../prisma/client');
593
+ const acl = require('./acl');
594
+
595
+ // Standard Prisma Client
596
+ const prisma = new PrismaClient({
597
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
598
+ });
599
+
600
+ module.exports = {
601
+ prisma,
602
+ PrismaClient,
603
+ acl
604
+ };
605
+ `;
606
+ }
573
607
 
574
608
  // Ensure rapidd directory exists
575
609
  const rapiddDir = path.dirname(rapiddJsPath);
@@ -636,14 +670,14 @@ async function updateRelationshipsForModel(filteredModels, relationshipsPath, pr
636
670
  }
637
671
 
638
672
  /**
639
- * Update rls.js for a specific model
673
+ * Update acl.js for a specific model
640
674
  */
641
- async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource, userTable, relationships, debug = false) {
642
- const { generateRLS } = require('../generators/rlsGeneratorV2');
675
+ async function updateACLForModel(filteredModels, allModels, aclPath, datasource, userTable, relationships, debug = false) {
676
+ const { generateACL } = require('../generators/aclGenerator');
643
677
 
644
- // Generate RLS for the filtered model (but pass all models for user table detection)
645
- const tempPath = rlsPath + '.tmp';
646
- await generateRLS(
678
+ // Generate ACL for the filtered model (but pass all models for user table detection)
679
+ const tempPath = aclPath + '.tmp';
680
+ await generateACL(
647
681
  filteredModels,
648
682
  tempPath,
649
683
  datasource.url,
@@ -654,11 +688,11 @@ async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource,
654
688
  allModels
655
689
  );
656
690
 
657
- // Read the generated RLS for the specific model
691
+ // Read the generated ACL for the specific model
658
692
  const tempContent = fs.readFileSync(tempPath, 'utf8');
659
693
  fs.unlinkSync(tempPath);
660
694
 
661
- // Extract the model's RLS configuration
695
+ // Extract the model's ACL configuration
662
696
  const modelName = Object.keys(filteredModels)[0];
663
697
 
664
698
  // Find the start of the model definition
@@ -695,44 +729,43 @@ async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource,
695
729
 
696
730
  if (braceCount === 0) {
697
731
  // Found the closing brace
698
- const modelRls = tempContent.substring(modelStart, i + 1);
699
732
  break;
700
733
  }
701
734
  }
702
735
  }
703
736
 
704
737
  if (braceCount !== 0) {
705
- throw new Error(`Could not extract RLS for model ${modelName} - unmatched braces`);
738
+ throw new Error(`Could not extract ACL for model ${modelName} - unmatched braces`);
706
739
  }
707
740
 
708
- const modelRls = tempContent.substring(modelStart, i + 1);
741
+ const modelAcl = tempContent.substring(modelStart, i + 1);
709
742
 
710
- // Read existing rls.js
711
- if (fs.existsSync(rlsPath)) {
712
- let existingContent = fs.readFileSync(rlsPath, 'utf8');
743
+ // Read existing acl.js
744
+ if (fs.existsSync(aclPath)) {
745
+ let existingContent = fs.readFileSync(aclPath, 'utf8');
713
746
 
714
- // Check if model already exists in RLS
747
+ // Check if model already exists in ACL
715
748
  const existingModelPattern = new RegExp(`${modelName}:\\s*\\{[\\s\\S]*?\\n \\}(?=,|\\n)`);
716
749
 
717
750
  if (existingModelPattern.test(existingContent)) {
718
- // Replace existing model RLS
719
- existingContent = existingContent.replace(existingModelPattern, modelRls);
751
+ // Replace existing model ACL
752
+ existingContent = existingContent.replace(existingModelPattern, modelAcl);
720
753
  } else {
721
- // Add new model RLS before the closing of rls.model
754
+ // Add new model ACL before the closing of acl.model
722
755
  // Find the last closing brace of a model object and add comma after it
723
756
  existingContent = existingContent.replace(
724
757
  /(\n \})\n(\};)/,
725
- `$1,\n ${modelRls}\n$2`
758
+ `$1,\n ${modelAcl}\n$2`
726
759
  );
727
760
  }
728
761
 
729
- fs.writeFileSync(rlsPath, existingContent);
762
+ fs.writeFileSync(aclPath, existingContent);
730
763
  console.log(`✓ Updated RLS for model: ${modelName}`);
731
764
  } else {
732
- // If rls.js doesn't exist, create it with just this model
733
- await generateRLS(
765
+ // If acl.js doesn't exist, create it with just this model
766
+ await generateACL(
734
767
  filteredModels,
735
- rlsPath,
768
+ aclPath,
736
769
  datasource.url,
737
770
  datasource.isPostgreSQL,
738
771
  userTable,
@@ -764,7 +797,7 @@ async function buildModels(options) {
764
797
  const modelJsPath = path.join(srcDir, 'Model.js');
765
798
  const rapiddDir = path.join(baseDir, 'rapidd');
766
799
  const relationshipsPath = path.join(rapiddDir, 'relationships.json');
767
- const rlsPath = path.join(rapiddDir, 'rls.js');
800
+ const aclPath = path.join(rapiddDir, 'acl.js');
768
801
  const rapiddJsPath = path.join(rapiddDir, 'rapidd.js');
769
802
  const routesDir = path.join(baseDir, 'routes', 'api', 'v1');
770
803
  const logsDir = path.join(baseDir, 'logs');
@@ -840,13 +873,13 @@ async function buildModels(options) {
840
873
  const shouldGenerate = {
841
874
  model: !options.only || options.only === 'model',
842
875
  route: !options.only || options.only === 'route',
843
- rls: !options.only || options.only === 'rls',
876
+ acl: !options.only || options.only === 'acl',
844
877
  relationship: !options.only || options.only === 'relationship'
845
878
  };
846
879
 
847
880
  // Validate --only option
848
- if (options.only && !['model', 'route', 'rls', 'relationship'].includes(options.only)) {
849
- throw new Error(`Invalid --only value "${options.only}". Must be one of: model, route, rls, relationship`);
881
+ if (options.only && !['model', 'route', 'acl', 'relationship'].includes(options.only)) {
882
+ throw new Error(`Invalid --only value "${options.only}". Must be one of: model, route, acl, relationship`);
850
883
  }
851
884
 
852
885
  // Generate model files
@@ -860,10 +893,18 @@ async function buildModels(options) {
860
893
  generateBaseModelFile(modelJsPath);
861
894
  }
862
895
 
896
+ // Parse datasource to determine database type
897
+ let datasource = { isPostgreSQL: true }; // Default to PostgreSQL
898
+ try {
899
+ datasource = parseDatasource(schemaPath);
900
+ } catch (error) {
901
+ console.warn('Could not parse datasource, assuming PostgreSQL:', error.message);
902
+ }
903
+
863
904
  // Generate rapidd/rapidd.js if it doesn't exist
864
905
  if (!fs.existsSync(rapiddJsPath)) {
865
906
  console.log('Generating rapidd/rapidd.js...');
866
- generateRapiddFile(rapiddJsPath);
907
+ generateRapiddFile(rapiddJsPath, datasource.isPostgreSQL);
867
908
  }
868
909
 
869
910
  // Generate relationships.json
@@ -889,9 +930,9 @@ async function buildModels(options) {
889
930
  }
890
931
  }
891
932
 
892
- // Generate RLS configuration
893
- if (shouldGenerate.rls) {
894
- console.log(`\nGenerating RLS configuration...`);
933
+ // Generate ACL configuration
934
+ if (shouldGenerate.acl) {
935
+ console.log(`\nGenerating ACL configuration...`);
895
936
 
896
937
  // Load relationships for Prisma filter building
897
938
  let relationships = {};
@@ -904,21 +945,19 @@ async function buildModels(options) {
904
945
  }
905
946
 
906
947
  try {
907
- // Parse datasource from Prisma schema to get database URL
908
- const datasource = parseDatasource(schemaPath);
909
948
 
910
- // For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive RLS
949
+ // For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive ACL
911
950
  if (!datasource.isPostgreSQL) {
912
- console.log(`${datasource.provider || 'Non-PostgreSQL'} database detected - generating permissive RLS...`);
913
- await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
951
+ console.log(`${datasource.provider || 'Non-PostgreSQL'} database detected - generating permissive ACL...`);
952
+ await generateACL(models, aclPath, null, false, options.userTable, relationships, options.debug);
914
953
  } else if (options.model) {
915
- // Update only specific model in rls.js
916
- await updateRLSForModel(filteredModels, models, rlsPath, datasource, options.userTable, relationships, options.debug);
954
+ // Update only specific model in acl.js
955
+ await updateACLForModel(filteredModels, models, aclPath, datasource, options.userTable, relationships, options.debug);
917
956
  } else {
918
- // Generate RLS for all models
919
- await generateRLS(
957
+ // Generate ACL for all models
958
+ await generateACL(
920
959
  models,
921
- rlsPath,
960
+ aclPath,
922
961
  datasource.url,
923
962
  datasource.isPostgreSQL,
924
963
  options.userTable,
@@ -927,10 +966,10 @@ async function buildModels(options) {
927
966
  );
928
967
  }
929
968
  } catch (error) {
930
- console.error('Failed to generate RLS:', error.message);
931
- console.log('Generating permissive RLS fallback...');
969
+ console.error('Failed to generate ACL:', error.message);
970
+ console.log('Generating permissive ACL fallback...');
932
971
  // Pass null for URL and false for isPostgreSQL to skip database connection
933
- await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
972
+ await generateACL(models, aclPath, null, false, options.userTable, relationships, options.debug);
934
973
  }
935
974
  }
936
975
 
@@ -30,7 +30,7 @@ function detectUserTable(models, userTableOption) {
30
30
  }
31
31
 
32
32
  /**
33
- * Extract RLS policies from PostgreSQL
33
+ * Extract ACL policies from PostgreSQL RLS
34
34
  */
35
35
  async function extractPostgreSQLPolicies(databaseUrl, models) {
36
36
  const client = new Client({ connectionString: databaseUrl });
@@ -48,7 +48,7 @@ async function extractPostgreSQLPolicies(databaseUrl, models) {
48
48
  policies[modelName] = [];
49
49
  }
50
50
 
51
- // Query all RLS policies from pg_policies
51
+ // Query all policies from PostgreSQL RLS (pg_policies)
52
52
  const result = await client.query(`
53
53
  SELECT
54
54
  tablename,
@@ -92,16 +92,15 @@ async function extractPostgreSQLPolicies(databaseUrl, models) {
92
92
  }
93
93
 
94
94
  /**
95
- * Generate RLS functions for a single model from PostgreSQL policies
95
+ * Generate ACL functions for a single model from PostgreSQL policies
96
96
  */
97
- function generateModelRLS(modelName, policies, converter) {
97
+ function generateModelACL(modelName, policies, converter) {
98
98
  const hasPolicies = policies && policies.length > 0;
99
99
 
100
100
  if (!hasPolicies) {
101
- // No RLS policies - generate permissive access
101
+ // No policies - generate permissive access
102
102
  return ` ${modelName}: {
103
103
  canCreate: (user) => true,
104
- hasAccess: (data, user) => true,
105
104
  getAccessFilter: (user) => ({}),
106
105
  getUpdateFilter: (user) => ({}),
107
106
  getDeleteFilter: (user) => ({}),
@@ -117,7 +116,6 @@ function generateModelRLS(modelName, policies, converter) {
117
116
 
118
117
  // Generate each function
119
118
  const canCreateCode = generateFunction(insertPolicies, 'withCheck', converter, modelName);
120
- const hasAccessCode = generateFunction(selectPolicies, 'using', converter, modelName);
121
119
  const accessFilterCode = generateFilter(selectPolicies, 'using', converter, modelName);
122
120
  let updateFilterCode = generateFilter(updatePolicies, 'using', converter, modelName);
123
121
  let deleteFilterCode = generateFilter(deletePolicies, 'using', converter, modelName);
@@ -136,9 +134,6 @@ function generateModelRLS(modelName, policies, converter) {
136
134
  canCreate: (user) => {
137
135
  ${canCreateCode}
138
136
  },
139
- hasAccess: (data, user) => {
140
- ${hasAccessCode}
141
- },
142
137
  getAccessFilter: (user) => {
143
138
  ${accessFilterCode}
144
139
  },
@@ -173,7 +168,7 @@ function generateFunction(policies, expressionField, converter, modelName) {
173
168
  console.log(`✓ Policy '${policy.name}': ${expr.substring(0, 50)}... -> ${jsExpr.substring(0, 80)}`);
174
169
  conditions.push(jsExpr);
175
170
  } catch (e) {
176
- console.warn(`⚠ Failed to convert RLS policy '${policy.name}' for ${modelName}: ${e.message}`);
171
+ console.warn(`⚠ Failed to convert policy '${policy.name}' for ${modelName}: ${e.message}`);
177
172
  console.warn(` SQL: ${expr}`);
178
173
  conditions.push(`true /* TODO: Manual conversion needed for policy '${policy.name}' */`);
179
174
  }
@@ -221,7 +216,7 @@ function generateFilter(policies, expressionField, converter, modelName) {
221
216
  hasDataFilter: prismaFilter !== '{}'
222
217
  });
223
218
  } catch (e) {
224
- console.warn(`⚠ Failed to convert RLS filter policy '${policy.name}' for ${modelName}: ${e.message}`);
219
+ console.warn(`⚠ Failed to convert filter policy '${policy.name}' for ${modelName}: ${e.message}`);
225
220
  console.warn(` SQL: ${expr}`);
226
221
  // On error, skip filter (fail-safe - no access)
227
222
  }
@@ -298,9 +293,9 @@ function buildConditionalFilter(filtersWithRoles) {
298
293
  }
299
294
 
300
295
  /**
301
- * Generate complete rls.js file
296
+ * Generate complete acl.js file
302
297
  */
303
- async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}, debug = false, allModels = null) {
298
+ async function generateACL(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}, debug = false, allModels = null) {
304
299
  // Use allModels for user table detection if provided (when filtering by model)
305
300
  const modelsForUserDetection = allModels || models;
306
301
  const userTable = detectUserTable(modelsForUserDetection, userTableOption);
@@ -309,7 +304,7 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
309
304
  let policies = {};
310
305
  const timestamp = new Date().toISOString();
311
306
 
312
- let rlsCode = `const rls = {\n model: {},\n lastUpdateDate: '${timestamp}'\n};\n\n`;
307
+ let aclCode = `const acl = {\n model: {},\n lastUpdateDate: '${timestamp}'\n};\n\n`;
313
308
 
314
309
  // Create enhanced converter with analyzed functions, models, and relationships
315
310
  let converter = createEnhancedConverter({}, {}, models, relationships);
@@ -332,15 +327,15 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
332
327
 
333
328
  // Save function analysis for debugging (only if --debug flag is set)
334
329
  if (debug) {
335
- const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
330
+ const configPath = path.join(path.dirname(outputPath), 'acl-mappings.json');
336
331
  const mappingConfig = generateMappingConfig(functionAnalysis);
337
332
  fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
338
333
  console.log(`✓ Function mappings saved to ${configPath}`);
339
334
  }
340
335
 
341
- // Also add user context requirements as a comment in rls.js
336
+ // Also add user context requirements as a comment in acl.js
342
337
  if (Object.keys(functionAnalysis.userContextRequirements).length > 0) {
343
- rlsCode = `/**
338
+ aclCode = `/**
344
339
  * User Context Requirements:
345
340
  * The user object should contain:
346
341
  ${Object.entries(functionAnalysis.userContextRequirements)
@@ -348,7 +343,7 @@ ${Object.entries(functionAnalysis.userContextRequirements)
348
343
  .join('\n')}
349
344
  */
350
345
 
351
- ` + rlsCode;
346
+ ` + aclCode;
352
347
  }
353
348
  } catch (error) {
354
349
  console.warn(`⚠ Could not analyze functions: ${error.message}`);
@@ -358,32 +353,32 @@ ${Object.entries(functionAnalysis.userContextRequirements)
358
353
  try {
359
354
  policies = await extractPostgreSQLPolicies(databaseUrl, models);
360
355
  const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
361
- console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
356
+ console.log(`✓ Extracted ${totalPolicies} policies from PostgreSQL RLS`);
362
357
  } catch (error) {
363
- console.warn(`⚠ Failed to extract PostgreSQL RLS: ${error.message}`);
364
- console.log('Generating permissive RLS for all models...');
358
+ console.warn(`⚠ Failed to extract PostgreSQL policies: ${error.message}`);
359
+ console.log('Generating permissive ACL for all models...');
365
360
  for (const modelName of modelNames) {
366
361
  policies[modelName] = [];
367
362
  }
368
363
  }
369
364
  } else {
370
365
  if (!isPostgreSQL) {
371
- console.log('Non-PostgreSQL database detected - RLS not supported');
366
+ console.log('Non-PostgreSQL database detected - generating permissive ACL');
372
367
  }
373
- console.log('Generating permissive RLS for all models...');
368
+ console.log('Generating permissive ACL for all models...');
374
369
  for (const modelName of modelNames) {
375
370
  policies[modelName] = [];
376
371
  }
377
372
  }
378
373
 
379
- // Generate RLS for each model
380
- rlsCode += 'rls.model = {\n';
381
- const modelRLSCode = modelNames.map(modelName => {
382
- return generateModelRLS(modelName, policies[modelName], converter);
374
+ // Generate ACL for each model
375
+ aclCode += 'acl.model = {\n';
376
+ const modelACLCode = modelNames.map(modelName => {
377
+ return generateModelACL(modelName, policies[modelName], converter);
383
378
  });
384
- rlsCode += modelRLSCode.join(',\n');
385
- rlsCode += '\n};\n\n';
386
- rlsCode += 'module.exports = rls;\n';
379
+ aclCode += modelACLCode.join(',\n');
380
+ aclCode += '\n};\n\n';
381
+ aclCode += 'module.exports = acl;\n';
387
382
 
388
383
  // Ensure output directory exists
389
384
  const outputDir = path.dirname(outputPath);
@@ -391,10 +386,10 @@ ${Object.entries(functionAnalysis.userContextRequirements)
391
386
  fs.mkdirSync(outputDir, { recursive: true });
392
387
  }
393
388
 
394
- fs.writeFileSync(outputPath, rlsCode);
395
- console.log('✓ Generated rls.js with dynamic function mappings');
389
+ fs.writeFileSync(outputPath, aclCode);
390
+ console.log('✓ Generated acl.js with dynamic function mappings');
396
391
  }
397
392
 
398
393
  module.exports = {
399
- generateRLS
394
+ generateACL
400
395
  };
@@ -26,12 +26,8 @@ router.all('*', async (req, res, next) => {
26
26
  router.get('/', async function(req, res) {
27
27
  try {
28
28
  const { q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc" } = req.query;
29
-
30
- const _data = req.${className}.getMany(q, include, limit, offset, sortBy, sortOrder);
31
- const _count = req.${className}.count(q);
32
- const [data, count] = await Promise.all([_data, _count]);
33
-
34
- return res.sendList(data, {'take': req.${className}.take(Number(limit)), 'skip': req.${className}.skip(Number(offset)), 'total': count});
29
+ const results = await req.${className}.getMany(q, include, limit, offset, sortBy, sortOrder);
30
+ return res.sendList(results.data, {'take': req.${className}.take(Number(limit)), 'skip': req.${className}.skip(Number(offset)), 'total': results.total});
35
31
  }
36
32
  catch(error){
37
33
  const response = QueryBuilder.errorHandler(error);
@@ -1,341 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { Client } = require('pg');
4
- const { createConverter } = require('../parsers/autoRLSConverter');
5
- const { analyzeFunctions, generateMappingConfig } = require('../parsers/functionAnalyzer');
6
-
7
- /**
8
- * Auto-detect user table name (case-insensitive search for user/users)
9
- * @param {Object} models - Models object from parser
10
- * @param {string} userTableOption - User-specified table name (optional)
11
- * @returns {string} - Name of the user table
12
- */
13
- function detectUserTable(models, userTableOption) {
14
- if (userTableOption) {
15
- return userTableOption;
16
- }
17
-
18
- const modelNames = Object.keys(models);
19
- const userTables = modelNames.filter(name =>
20
- name.toLowerCase() === 'user' || name.toLowerCase() === 'users'
21
- );
22
-
23
- if (userTables.length === 0) {
24
- throw new Error('No user table found (user/users). Please specify --user-table option.');
25
- }
26
-
27
- if (userTables.length > 1) {
28
- throw new Error(`Multiple user tables found: ${userTables.join(', ')}. Please specify --user-table option.`);
29
- }
30
-
31
- return userTables[0];
32
- }
33
-
34
- /**
35
- * Extract RLS policies from PostgreSQL
36
- * @param {string} databaseUrl - PostgreSQL connection URL
37
- * @param {Object} models - Models object with dbName mapping
38
- * @returns {Object} - RLS policies for each model
39
- */
40
- async function extractPostgreSQLPolicies(databaseUrl, models) {
41
- const client = new Client({ connectionString: databaseUrl });
42
-
43
- try {
44
- await client.connect();
45
-
46
- const policies = {};
47
-
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;
53
- policies[modelName] = [];
54
- }
55
-
56
- // Query all RLS policies from pg_policies
57
- const result = await client.query(`
58
- SELECT
59
- tablename,
60
- policyname,
61
- permissive,
62
- roles,
63
- cmd,
64
- qual,
65
- with_check
66
- FROM pg_policies
67
- WHERE schemaname = 'public'
68
- ORDER BY tablename, policyname
69
- `);
70
-
71
- // Group policies by model (using table to model mapping)
72
- for (const row of result.rows) {
73
- const tableName = row.tablename;
74
- const modelName = tableToModelMap[tableName];
75
-
76
- if (modelName && policies[modelName] !== undefined) {
77
- policies[modelName].push({
78
- name: row.policyname,
79
- permissive: row.permissive === 'PERMISSIVE',
80
- roles: row.roles,
81
- command: row.cmd, // SELECT, INSERT, UPDATE, DELETE, ALL
82
- using: row.qual, // USING expression
83
- withCheck: row.with_check // WITH CHECK expression
84
- });
85
- }
86
- }
87
-
88
- await client.end();
89
- return policies;
90
-
91
- } catch (error) {
92
- try {
93
- await client.end();
94
- } catch (e) {
95
- // Ignore cleanup errors
96
- }
97
- throw error;
98
- }
99
- }
100
-
101
- /**
102
- * Generate RLS functions for a single model from PostgreSQL policies
103
- * @param {string} modelName - Name of the model
104
- * @param {Array} policies - Array of policy objects for this model
105
- * @param {string} userTable - Name of the user table
106
- * @returns {string} - JavaScript code for RLS functions
107
- */
108
- function generateModelRLS(modelName, policies, userTable) {
109
- const hasPolicies = policies && policies.length > 0;
110
-
111
- if (!hasPolicies) {
112
- // No RLS policies - generate permissive access
113
- return ` ${modelName}: {
114
- canCreate: (user) => true,
115
- hasAccess: (data, user) => true,
116
- getAccessFilter: (user) => ({}),
117
- getUpdateFilter: (user) => ({}),
118
- getDeleteFilter: (user) => ({}),
119
- getOmitFields: (user) => []
120
- }`;
121
- }
122
-
123
- // Find policies by command type
124
- const selectPolicies = policies.filter(p => p.command === 'SELECT' || p.command === 'ALL');
125
- const insertPolicies = policies.filter(p => p.command === 'INSERT' || p.command === 'ALL');
126
- const updatePolicies = policies.filter(p => p.command === 'UPDATE' || p.command === 'ALL');
127
- const deletePolicies = policies.filter(p => p.command === 'DELETE' || p.command === 'ALL');
128
-
129
- // Generate canCreate (INSERT policies with WITH CHECK)
130
- const canCreateCode = generateCanCreate(insertPolicies);
131
-
132
- // Generate hasAccess (SELECT policies with USING)
133
- const hasAccessCode = generateHasAccess(selectPolicies);
134
-
135
- // Generate getAccessFilter (SELECT policies)
136
- const accessFilterCode = generateFilter(selectPolicies, 'using');
137
-
138
- // Generate getUpdateFilter (UPDATE policies)
139
- const updateFilterCode = generateFilter(updatePolicies, 'using');
140
-
141
- // Generate getDeleteFilter (DELETE policies)
142
- const deleteFilterCode = generateFilter(deletePolicies, 'using');
143
-
144
- return ` ${modelName}: {
145
- canCreate: (user) => {
146
- ${canCreateCode}
147
- },
148
- hasAccess: (data, user) => {
149
- ${hasAccessCode}
150
- },
151
- getAccessFilter: (user) => {
152
- ${accessFilterCode}
153
- },
154
- getUpdateFilter: (user) => {
155
- ${updateFilterCode}
156
- },
157
- getDeleteFilter: (user) => {
158
- ${deleteFilterCode}
159
- },
160
- getOmitFields: (user) => []
161
- }`;
162
- }
163
-
164
- /**
165
- * Generate canCreate function from INSERT policies
166
- */
167
- function generateCanCreate(insertPolicies, converter) {
168
- if (insertPolicies.length === 0) {
169
- return 'return true;';
170
- }
171
-
172
- const conditions = [];
173
-
174
- for (const policy of insertPolicies) {
175
- const expr = policy.withCheck || policy.using;
176
- if (expr) {
177
- try {
178
- const jsExpr = converter.convertToJavaScript(expr, 'data', 'user');
179
- conditions.push(jsExpr);
180
- } catch (e) {
181
- conditions.push(`true /* Error parsing: ${expr.substring(0, 50)}... */`);
182
- }
183
- }
184
- }
185
-
186
- if (conditions.length === 0) {
187
- return 'return true;';
188
- }
189
-
190
- // Policies are OR'd together (any policy allows)
191
- return `return ${conditions.join(' || ')};`;
192
- }
193
-
194
- /**
195
- * Generate hasAccess function from SELECT policies
196
- */
197
- function generateHasAccess(selectPolicies) {
198
- if (selectPolicies.length === 0) {
199
- return 'return true;';
200
- }
201
-
202
- const conditions = [];
203
-
204
- for (const policy of selectPolicies) {
205
- if (policy.using) {
206
- try {
207
- const jsExpr = convertToJavaScript(policy.using, 'data', 'user');
208
- conditions.push(jsExpr);
209
- } catch (e) {
210
- conditions.push(`true /* Error parsing: ${policy.using.substring(0, 50)}... */`);
211
- }
212
- }
213
- }
214
-
215
- if (conditions.length === 0) {
216
- return 'return true;';
217
- }
218
-
219
- // Policies are OR'd together (any policy allows)
220
- return `return ${conditions.join(' || ')};`;
221
- }
222
-
223
- /**
224
- * Generate Prisma filter function
225
- */
226
- function generateFilter(policies, expressionField) {
227
- if (policies.length === 0) {
228
- return 'return {};';
229
- }
230
-
231
- const filters = [];
232
-
233
- for (const policy of policies) {
234
- const expr = policy[expressionField];
235
- if (expr) {
236
- try {
237
- const prismaFilter = convertToPrismaFilter(expr, 'user');
238
- if (prismaFilter !== '{}') {
239
- filters.push(prismaFilter);
240
- }
241
- } catch (e) {
242
- // On error, return empty filter (permissive)
243
- }
244
- }
245
- }
246
-
247
- if (filters.length === 0) {
248
- return 'return {};';
249
- }
250
-
251
- if (filters.length === 1) {
252
- return `return ${filters[0]};`;
253
- }
254
-
255
- // Multiple policies are OR'd together
256
- return `return { OR: [${filters.join(', ')}] };`;
257
- }
258
-
259
- /**
260
- * Generate complete rls.js file
261
- * @param {Object} models - Models object
262
- * @param {string} outputPath - Path to output rls.js
263
- * @param {string} databaseUrl - Database connection URL
264
- * @param {boolean} isPostgreSQL - Whether database is PostgreSQL
265
- * @param {string} userTableOption - User-specified table name
266
- */
267
- async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, debug = false) {
268
- const userTable = detectUserTable(models, userTableOption);
269
- const modelNames = Object.keys(models);
270
-
271
- let policies = {};
272
- const timestamp = new Date().toISOString();
273
-
274
- let rlsCode = `const rls = {\n model: {},\n lastUpdateDate: '${timestamp}'\n};\n\n`;
275
-
276
- // Analyze PostgreSQL functions if available
277
- let functionAnalysis = null;
278
-
279
- if (isPostgreSQL && databaseUrl) {
280
- console.log('PostgreSQL detected - analyzing database functions...');
281
- try {
282
- functionAnalysis = await analyzeFunctions(databaseUrl);
283
- console.log(`✓ Analyzed ${Object.keys(functionAnalysis.functionMappings).length} PostgreSQL functions`);
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
- }
292
- } catch (error) {
293
- console.warn(`⚠ Could not analyze functions: ${error.message}`);
294
- }
295
-
296
- console.log('Extracting RLS policies from database...');
297
- try {
298
- policies = await extractPostgreSQLPolicies(databaseUrl, models);
299
- const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
300
- console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
301
- } catch (error) {
302
- console.warn(`⚠ Failed to extract PostgreSQL RLS: ${error.message}`);
303
- console.log('Generating permissive RLS for all models...');
304
- // Initialize empty policies for all models
305
- for (const modelName of modelNames) {
306
- policies[modelName] = [];
307
- }
308
- }
309
- } else {
310
- if (!isPostgreSQL) {
311
- console.log('Non-PostgreSQL database detected (MySQL/SQLite/etc) - RLS not supported');
312
- }
313
- console.log('Generating permissive RLS for all models...');
314
- // Initialize empty policies for all models
315
- for (const modelName of modelNames) {
316
- policies[modelName] = [];
317
- }
318
- }
319
-
320
- // Generate RLS for each model
321
- rlsCode += 'rls.model = {\n';
322
- const modelRLSCode = modelNames.map(modelName => {
323
- return generateModelRLS(modelName, policies[modelName], userTable);
324
- });
325
- rlsCode += modelRLSCode.join(',\n');
326
- rlsCode += '\n};\n\n';
327
- rlsCode += 'module.exports = rls;\n';
328
-
329
- // Ensure output directory exists
330
- const outputDir = path.dirname(outputPath);
331
- if (!fs.existsSync(outputDir)) {
332
- fs.mkdirSync(outputDir, { recursive: true });
333
- }
334
-
335
- fs.writeFileSync(outputPath, rlsCode);
336
- console.log('Generated rls.js');
337
- }
338
-
339
- module.exports = {
340
- generateRLS
341
- };