@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 +12 -18
- package/bin/cli.js +3 -3
- package/package.json +1 -1
- package/src/commands/build.js +122 -78
- package/src/generators/{rlsGeneratorV2.js → aclGenerator.js} +29 -34
- package/src/generators/routeGenerator.js +2 -6
- package/src/generators/rlsGenerator.js +0 -341
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
|
|
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", "
|
|
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/
|
|
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
|
|
77
|
-
npx rapidd build --only
|
|
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
|
|
93
|
-
npx rapidd build --model account --only
|
|
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
|
-
├──
|
|
109
|
+
├── acl.js
|
|
110
110
|
├── relationships.json
|
|
111
111
|
└── rapidd.js
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
##
|
|
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
|
|
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
|
|
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", "
|
|
21
|
-
.option('--user-table <name>', 'Name of the user table for
|
|
22
|
-
.option('--debug', 'Enable debug mode (generates
|
|
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
package/src/commands/build.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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.
|
|
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
|
-
//
|
|
36
|
-
_canCreate = () => this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
678
|
+
* Update acl.js for a specific model
|
|
640
679
|
*/
|
|
641
|
-
async function
|
|
642
|
-
const {
|
|
680
|
+
async function updateACLForModel(filteredModels, allModels, aclPath, datasource, userTable, relationships, debug = false) {
|
|
681
|
+
const { generateACL } = require('../generators/aclGenerator');
|
|
643
682
|
|
|
644
|
-
// Generate
|
|
645
|
-
const tempPath =
|
|
646
|
-
await
|
|
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
|
|
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
|
|
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
|
|
743
|
+
throw new Error(`Could not extract ACL for model ${modelName} - unmatched braces`);
|
|
706
744
|
}
|
|
707
745
|
|
|
708
|
-
const
|
|
746
|
+
const modelAcl = tempContent.substring(modelStart, i + 1);
|
|
709
747
|
|
|
710
|
-
// Read existing
|
|
711
|
-
if (fs.existsSync(
|
|
712
|
-
let existingContent = fs.readFileSync(
|
|
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
|
|
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
|
|
719
|
-
existingContent = existingContent.replace(existingModelPattern,
|
|
756
|
+
// Replace existing model ACL
|
|
757
|
+
existingContent = existingContent.replace(existingModelPattern, modelAcl);
|
|
720
758
|
} else {
|
|
721
|
-
// Add new 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 ${
|
|
763
|
+
`$1,\n ${modelAcl}\n$2`
|
|
726
764
|
);
|
|
727
765
|
}
|
|
728
766
|
|
|
729
|
-
fs.writeFileSync(
|
|
767
|
+
fs.writeFileSync(aclPath, existingContent);
|
|
730
768
|
console.log(`✓ Updated RLS for model: ${modelName}`);
|
|
731
769
|
} else {
|
|
732
|
-
// If
|
|
733
|
-
await
|
|
770
|
+
// If acl.js doesn't exist, create it with just this model
|
|
771
|
+
await generateACL(
|
|
734
772
|
filteredModels,
|
|
735
|
-
|
|
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
|
|
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
|
-
|
|
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', '
|
|
849
|
-
throw new Error(`Invalid --only value "${options.only}". Must be one of: model, route,
|
|
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
|
|
893
|
-
if (shouldGenerate.
|
|
894
|
-
console.log(`\nGenerating
|
|
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
|
|
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
|
|
913
|
-
await
|
|
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
|
|
916
|
-
await
|
|
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
|
|
919
|
-
await
|
|
962
|
+
// Generate ACL for all models
|
|
963
|
+
await generateACL(
|
|
920
964
|
models,
|
|
921
|
-
|
|
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
|
|
931
|
-
console.log('Generating permissive
|
|
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
|
|
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
|
|
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
|
|
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
|
|
95
|
+
* Generate ACL functions for a single model from PostgreSQL policies
|
|
96
96
|
*/
|
|
97
|
-
function
|
|
97
|
+
function generateModelACL(modelName, policies, converter) {
|
|
98
98
|
const hasPolicies = policies && policies.length > 0;
|
|
99
99
|
|
|
100
100
|
if (!hasPolicies) {
|
|
101
|
-
// No
|
|
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
|
|
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
|
|
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
|
|
296
|
+
* Generate complete acl.js file
|
|
302
297
|
*/
|
|
303
|
-
async function
|
|
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
|
|
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), '
|
|
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
|
|
336
|
+
// Also add user context requirements as a comment in acl.js
|
|
342
337
|
if (Object.keys(functionAnalysis.userContextRequirements).length > 0) {
|
|
343
|
-
|
|
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
|
-
` +
|
|
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}
|
|
356
|
+
console.log(`✓ Extracted ${totalPolicies} policies from PostgreSQL RLS`);
|
|
362
357
|
} catch (error) {
|
|
363
|
-
console.warn(`⚠ Failed to extract PostgreSQL
|
|
364
|
-
console.log('Generating permissive
|
|
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 -
|
|
366
|
+
console.log('Non-PostgreSQL database detected - generating permissive ACL');
|
|
372
367
|
}
|
|
373
|
-
console.log('Generating permissive
|
|
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
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
return
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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,
|
|
395
|
-
console.log('✓ Generated
|
|
389
|
+
fs.writeFileSync(outputPath, aclCode);
|
|
390
|
+
console.log('✓ Generated acl.js with dynamic function mappings');
|
|
396
391
|
}
|
|
397
392
|
|
|
398
393
|
module.exports = {
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
};
|