@rapidd/build 1.0.7 → 1.1.0

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/README.md CHANGED
@@ -5,7 +5,7 @@ Dynamic code generator that transforms Prisma schemas into complete Express.js C
5
5
  ## Features
6
6
 
7
7
  - 🚀 **Automatic CRUD API Generation** - Creates Express.js routes from Prisma models
8
- - 🔒 **RLS Translation** - Converts PostgreSQL Row-Level Security policies to JavaScript/Prisma filters
8
+ - 🔒 **RLS Translation** - Converts PostgreSQL Row-Level Security policies to JavaScript/Prisma filters (ACL)
9
9
  - 🎯 **Dynamic & Schema-Aware** - Zero hardcoding, adapts to any database structure
10
10
  - 🔗 **Relationship Handling** - Supports 1:1, 1:n, n:m including junction tables
11
11
  - 👥 **Role-Based Access Control** - Properly handles role checks in filters
@@ -34,7 +34,7 @@ npx rapidd build --model user
34
34
  # Generate only specific component
35
35
  npx rapidd build --only model
36
36
  npx rapidd build --only route
37
- npx rapidd build --only rls
37
+ npx rapidd build --only acl
38
38
  npx rapidd build --only relationship
39
39
 
40
40
  # Combine model and component filters
@@ -49,7 +49,7 @@ npx rapidd build --user-table accounts
49
49
  - `-o, --output <path>` - Output directory (default: `./`)
50
50
  - `-s, --schema <path>` - Prisma schema file (default: `./prisma/schema.prisma`)
51
51
  - `-m, --model <name>` - Generate/update only specific model (e.g., "account", "user")
52
- - `--only <component>` - Generate only specific component: "model", "route", "rls", or "relationship"
52
+ - `--only <component>` - Generate only specific component: "model", "route", "acl", or "relationship"
53
53
  - `--user-table <name>` - User table name for RLS (default: auto-detected)
54
54
 
55
55
  ## Selective Generation
@@ -65,7 +65,7 @@ This will:
65
65
  - Generate/update `src/Model/Account.js`
66
66
  - Generate/update `routes/api/v1/account.js`
67
67
  - Update the `account` entry in `rapidd/relationships.json`
68
- - Update the `account` entry in `rapidd/rls.js`
68
+ - Update the `account` entry in `rapidd/acl.js`
69
69
 
70
70
  ### Update Single Component
71
71
 
@@ -73,8 +73,8 @@ This will:
73
73
  # Regenerate all routes
74
74
  npx rapidd build --only route
75
75
 
76
- # Regenerate all RLS configs
77
- npx rapidd build --only rls
76
+ # Regenerate all ACL configs
77
+ npx rapidd build --only acl
78
78
 
79
79
  # Regenerate all models
80
80
  npx rapidd build --only model
@@ -89,8 +89,8 @@ npx rapidd build --only relationship
89
89
  # Update only the route for a specific model
90
90
  npx rapidd build --model user --only route
91
91
 
92
- # Update RLS for account model
93
- npx rapidd build --model account --only rls
92
+ # Update ACL for account model
93
+ npx rapidd build --model account --only acl
94
94
  ```
95
95
 
96
96
  ## Generated Structure
@@ -106,12 +106,12 @@ npx rapidd build --model account --only rls
106
106
  │ ├── post.js
107
107
  │ └── ...
108
108
  └── rapidd/
109
- ├── rls.js
109
+ ├── acl.js
110
110
  ├── relationships.json
111
111
  └── rapidd.js
112
112
  ```
113
113
 
114
- ## RLS Translation Example
114
+ ## ACL Translation Example
115
115
 
116
116
  **PostgreSQL Policy:**
117
117
  ```sql
@@ -131,12 +131,6 @@ getAccessFilter: (user) => {
131
131
  }
132
132
  ```
133
133
 
134
- ## Usage with PostgreSQL RLS
135
-
136
- ```bash
137
- DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" npx rapidd build
138
- ```
139
-
140
134
  ## Use Cases
141
135
 
142
136
  ### During Development
@@ -148,7 +142,7 @@ npx rapidd build --model newModel
148
142
  npx rapidd build --only relationship
149
143
 
150
144
  # After updating RLS policies
151
- npx rapidd build --only rls
145
+ npx rapidd build --only acl
152
146
  ```
153
147
 
154
148
  ### Continuous Integration
@@ -161,7 +155,7 @@ npx rapidd build --output ./generated
161
155
  ```bash
162
156
  # Update specific model after schema changes
163
157
  npx rapidd build --model user --only model
164
- npx rapidd build --model user --only rls
158
+ npx rapidd build --model user --only acl
165
159
  ```
166
160
 
167
161
  ## License
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.1.0",
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,31 @@ module.exports = {
567
584
  prismaWithRLS,
568
585
  getRLSConfig,
569
586
  setRLSVariables,
570
- rls
587
+ acl
588
+ };
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
+ const prismaTransaction = async (operations) => prisma.$transaction(async (tx) => {
601
+ return Promise.all(operations.map(op => op(tx)));
602
+ });
603
+
604
+ module.exports = {
605
+ prisma,
606
+ prismaTransaction,
607
+ PrismaClient,
608
+ acl
571
609
  };
572
610
  `;
611
+ }
573
612
 
574
613
  // Ensure rapidd directory exists
575
614
  const rapiddDir = path.dirname(rapiddJsPath);
@@ -636,14 +675,14 @@ async function updateRelationshipsForModel(filteredModels, relationshipsPath, pr
636
675
  }
637
676
 
638
677
  /**
639
- * Update rls.js for a specific model
678
+ * Update acl.js for a specific model
640
679
  */
641
- async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource, userTable, relationships, debug = false) {
642
- const { generateRLS } = require('../generators/rlsGeneratorV2');
680
+ async function updateACLForModel(filteredModels, allModels, aclPath, datasource, userTable, relationships, debug = false) {
681
+ const { generateACL } = require('../generators/aclGenerator');
643
682
 
644
- // Generate RLS for the filtered model (but pass all models for user table detection)
645
- const tempPath = rlsPath + '.tmp';
646
- await generateRLS(
683
+ // Generate ACL for the filtered model (but pass all models for user table detection)
684
+ const tempPath = aclPath + '.tmp';
685
+ await generateACL(
647
686
  filteredModels,
648
687
  tempPath,
649
688
  datasource.url,
@@ -654,11 +693,11 @@ async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource,
654
693
  allModels
655
694
  );
656
695
 
657
- // Read the generated RLS for the specific model
696
+ // Read the generated ACL for the specific model
658
697
  const tempContent = fs.readFileSync(tempPath, 'utf8');
659
698
  fs.unlinkSync(tempPath);
660
699
 
661
- // Extract the model's RLS configuration
700
+ // Extract the model's ACL configuration
662
701
  const modelName = Object.keys(filteredModels)[0];
663
702
 
664
703
  // Find the start of the model definition
@@ -695,44 +734,43 @@ async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource,
695
734
 
696
735
  if (braceCount === 0) {
697
736
  // Found the closing brace
698
- const modelRls = tempContent.substring(modelStart, i + 1);
699
737
  break;
700
738
  }
701
739
  }
702
740
  }
703
741
 
704
742
  if (braceCount !== 0) {
705
- throw new Error(`Could not extract RLS for model ${modelName} - unmatched braces`);
743
+ throw new Error(`Could not extract ACL for model ${modelName} - unmatched braces`);
706
744
  }
707
745
 
708
- const modelRls = tempContent.substring(modelStart, i + 1);
746
+ const modelAcl = tempContent.substring(modelStart, i + 1);
709
747
 
710
- // Read existing rls.js
711
- if (fs.existsSync(rlsPath)) {
712
- let existingContent = fs.readFileSync(rlsPath, 'utf8');
748
+ // Read existing acl.js
749
+ if (fs.existsSync(aclPath)) {
750
+ let existingContent = fs.readFileSync(aclPath, 'utf8');
713
751
 
714
- // Check if model already exists in RLS
752
+ // Check if model already exists in ACL
715
753
  const existingModelPattern = new RegExp(`${modelName}:\\s*\\{[\\s\\S]*?\\n \\}(?=,|\\n)`);
716
754
 
717
755
  if (existingModelPattern.test(existingContent)) {
718
- // Replace existing model RLS
719
- existingContent = existingContent.replace(existingModelPattern, modelRls);
756
+ // Replace existing model ACL
757
+ existingContent = existingContent.replace(existingModelPattern, modelAcl);
720
758
  } else {
721
- // Add new model RLS before the closing of rls.model
759
+ // Add new model ACL before the closing of acl.model
722
760
  // Find the last closing brace of a model object and add comma after it
723
761
  existingContent = existingContent.replace(
724
762
  /(\n \})\n(\};)/,
725
- `$1,\n ${modelRls}\n$2`
763
+ `$1,\n ${modelAcl}\n$2`
726
764
  );
727
765
  }
728
766
 
729
- fs.writeFileSync(rlsPath, existingContent);
767
+ fs.writeFileSync(aclPath, existingContent);
730
768
  console.log(`✓ Updated RLS for model: ${modelName}`);
731
769
  } else {
732
- // If rls.js doesn't exist, create it with just this model
733
- await generateRLS(
770
+ // If acl.js doesn't exist, create it with just this model
771
+ await generateACL(
734
772
  filteredModels,
735
- rlsPath,
773
+ aclPath,
736
774
  datasource.url,
737
775
  datasource.isPostgreSQL,
738
776
  userTable,
@@ -764,7 +802,7 @@ async function buildModels(options) {
764
802
  const modelJsPath = path.join(srcDir, 'Model.js');
765
803
  const rapiddDir = path.join(baseDir, 'rapidd');
766
804
  const relationshipsPath = path.join(rapiddDir, 'relationships.json');
767
- const rlsPath = path.join(rapiddDir, 'rls.js');
805
+ const aclPath = path.join(rapiddDir, 'acl.js');
768
806
  const rapiddJsPath = path.join(rapiddDir, 'rapidd.js');
769
807
  const routesDir = path.join(baseDir, 'routes', 'api', 'v1');
770
808
  const logsDir = path.join(baseDir, 'logs');
@@ -840,13 +878,13 @@ async function buildModels(options) {
840
878
  const shouldGenerate = {
841
879
  model: !options.only || options.only === 'model',
842
880
  route: !options.only || options.only === 'route',
843
- rls: !options.only || options.only === 'rls',
881
+ acl: !options.only || options.only === 'acl',
844
882
  relationship: !options.only || options.only === 'relationship'
845
883
  };
846
884
 
847
885
  // 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`);
886
+ if (options.only && !['model', 'route', 'acl', 'relationship'].includes(options.only)) {
887
+ throw new Error(`Invalid --only value "${options.only}". Must be one of: model, route, acl, relationship`);
850
888
  }
851
889
 
852
890
  // Generate model files
@@ -860,10 +898,18 @@ async function buildModels(options) {
860
898
  generateBaseModelFile(modelJsPath);
861
899
  }
862
900
 
901
+ // Parse datasource to determine database type
902
+ let datasource = { isPostgreSQL: true }; // Default to PostgreSQL
903
+ try {
904
+ datasource = parseDatasource(schemaPath);
905
+ } catch (error) {
906
+ console.warn('Could not parse datasource, assuming PostgreSQL:', error.message);
907
+ }
908
+
863
909
  // Generate rapidd/rapidd.js if it doesn't exist
864
910
  if (!fs.existsSync(rapiddJsPath)) {
865
911
  console.log('Generating rapidd/rapidd.js...');
866
- generateRapiddFile(rapiddJsPath);
912
+ generateRapiddFile(rapiddJsPath, datasource.isPostgreSQL);
867
913
  }
868
914
 
869
915
  // Generate relationships.json
@@ -889,9 +935,9 @@ async function buildModels(options) {
889
935
  }
890
936
  }
891
937
 
892
- // Generate RLS configuration
893
- if (shouldGenerate.rls) {
894
- console.log(`\nGenerating RLS configuration...`);
938
+ // Generate ACL configuration
939
+ if (shouldGenerate.acl) {
940
+ console.log(`\nGenerating ACL configuration...`);
895
941
 
896
942
  // Load relationships for Prisma filter building
897
943
  let relationships = {};
@@ -904,21 +950,19 @@ async function buildModels(options) {
904
950
  }
905
951
 
906
952
  try {
907
- // Parse datasource from Prisma schema to get database URL
908
- const datasource = parseDatasource(schemaPath);
909
953
 
910
- // For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive RLS
954
+ // For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive ACL
911
955
  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);
956
+ console.log(`${datasource.provider || 'Non-PostgreSQL'} database detected - generating permissive ACL...`);
957
+ await generateACL(models, aclPath, null, false, options.userTable, relationships, options.debug);
914
958
  } else if (options.model) {
915
- // Update only specific model in rls.js
916
- await updateRLSForModel(filteredModels, models, rlsPath, datasource, options.userTable, relationships, options.debug);
959
+ // Update only specific model in acl.js
960
+ await updateACLForModel(filteredModels, models, aclPath, datasource, options.userTable, relationships, options.debug);
917
961
  } else {
918
- // Generate RLS for all models
919
- await generateRLS(
962
+ // Generate ACL for all models
963
+ await generateACL(
920
964
  models,
921
- rlsPath,
965
+ aclPath,
922
966
  datasource.url,
923
967
  datasource.isPostgreSQL,
924
968
  options.userTable,
@@ -927,10 +971,10 @@ async function buildModels(options) {
927
971
  );
928
972
  }
929
973
  } catch (error) {
930
- console.error('Failed to generate RLS:', error.message);
931
- console.log('Generating permissive RLS fallback...');
974
+ console.error('Failed to generate ACL:', error.message);
975
+ console.log('Generating permissive ACL fallback...');
932
976
  // Pass null for URL and false for isPostgreSQL to skip database connection
933
- await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
977
+ await generateACL(models, aclPath, null, false, options.userTable, relationships, options.debug);
934
978
  }
935
979
  }
936
980
 
@@ -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
- };