@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", "
|
|
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,26 @@ module.exports = {
|
|
|
567
584
|
prismaWithRLS,
|
|
568
585
|
getRLSConfig,
|
|
569
586
|
setRLSVariables,
|
|
570
|
-
|
|
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
|
|
673
|
+
* Update acl.js for a specific model
|
|
640
674
|
*/
|
|
641
|
-
async function
|
|
642
|
-
const {
|
|
675
|
+
async function updateACLForModel(filteredModels, allModels, aclPath, datasource, userTable, relationships, debug = false) {
|
|
676
|
+
const { generateACL } = require('../generators/aclGenerator');
|
|
643
677
|
|
|
644
|
-
// Generate
|
|
645
|
-
const tempPath =
|
|
646
|
-
await
|
|
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
|
|
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
|
|
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
|
|
738
|
+
throw new Error(`Could not extract ACL for model ${modelName} - unmatched braces`);
|
|
706
739
|
}
|
|
707
740
|
|
|
708
|
-
const
|
|
741
|
+
const modelAcl = tempContent.substring(modelStart, i + 1);
|
|
709
742
|
|
|
710
|
-
// Read existing
|
|
711
|
-
if (fs.existsSync(
|
|
712
|
-
let existingContent = fs.readFileSync(
|
|
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
|
|
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
|
|
719
|
-
existingContent = existingContent.replace(existingModelPattern,
|
|
751
|
+
// Replace existing model ACL
|
|
752
|
+
existingContent = existingContent.replace(existingModelPattern, modelAcl);
|
|
720
753
|
} else {
|
|
721
|
-
// Add new 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 ${
|
|
758
|
+
`$1,\n ${modelAcl}\n$2`
|
|
726
759
|
);
|
|
727
760
|
}
|
|
728
761
|
|
|
729
|
-
fs.writeFileSync(
|
|
762
|
+
fs.writeFileSync(aclPath, existingContent);
|
|
730
763
|
console.log(`✓ Updated RLS for model: ${modelName}`);
|
|
731
764
|
} else {
|
|
732
|
-
// If
|
|
733
|
-
await
|
|
765
|
+
// If acl.js doesn't exist, create it with just this model
|
|
766
|
+
await generateACL(
|
|
734
767
|
filteredModels,
|
|
735
|
-
|
|
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
|
|
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
|
-
|
|
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', '
|
|
849
|
-
throw new Error(`Invalid --only value "${options.only}". Must be one of: model, route,
|
|
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
|
|
893
|
-
if (shouldGenerate.
|
|
894
|
-
console.log(`\nGenerating
|
|
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
|
|
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
|
|
913
|
-
await
|
|
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
|
|
916
|
-
await
|
|
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
|
|
919
|
-
await
|
|
957
|
+
// Generate ACL for all models
|
|
958
|
+
await generateACL(
|
|
920
959
|
models,
|
|
921
|
-
|
|
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
|
|
931
|
-
console.log('Generating permissive
|
|
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
|
|
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
|
|
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
|
-
};
|