@rapidd/build 1.0.6 → 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,99 +351,259 @@ 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();
|
|
366
368
|
|
|
367
369
|
// RLS Configuration aus Environment Variables
|
|
368
370
|
const RLS_CONFIG = {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
371
|
+
namespace: process.env.RLS_NAMESPACE || 'app',
|
|
372
|
+
userId: process.env.RLS_USER_ID || 'current_user_id',
|
|
373
|
+
userRole: process.env.RLS_USER_ROLE || 'current_user_role',
|
|
372
374
|
};
|
|
373
375
|
|
|
374
376
|
// Basis Prisma Client
|
|
375
377
|
const basePrisma = new PrismaClient({
|
|
376
|
-
|
|
378
|
+
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
377
379
|
});
|
|
378
380
|
|
|
379
381
|
/**
|
|
380
|
-
* Setze RLS Session Variables in PostgreSQL
|
|
382
|
+
* FIXED: Setze RLS Session Variables in PostgreSQL
|
|
383
|
+
* Execute each SET command separately to avoid prepared statement error
|
|
381
384
|
*/
|
|
382
385
|
async function setRLSVariables(tx, userId, userRole) {
|
|
383
386
|
const namespace = RLS_CONFIG.namespace;
|
|
384
387
|
const userIdVar = RLS_CONFIG.userId;
|
|
385
388
|
const userRoleVar = RLS_CONFIG.userRole;
|
|
386
389
|
|
|
387
|
-
|
|
388
|
-
await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}
|
|
390
|
+
// Execute SET commands separately (PostgreSQL doesn't allow multiple commands in prepared statements)
|
|
391
|
+
await tx.$executeRawUnsafe(\`SET LOCAL "\${namespace}"."\${userIdVar}" = '\${userId}'\`);
|
|
392
|
+
await tx.$executeRawUnsafe(\`SET LOCAL "\${namespace}"."\${userRoleVar}" = '\${userRole}'\`);
|
|
389
393
|
}
|
|
390
394
|
|
|
391
|
-
// Erweiterter Prisma mit automatischer RLS
|
|
395
|
+
// FIXED: Erweiterter Prisma mit automatischer RLS
|
|
392
396
|
const prisma = basePrisma.$extends({
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
397
|
+
query: {
|
|
398
|
+
async $allOperations({ operation, args, query, model }) {
|
|
399
|
+
const context = requestContext.getStore();
|
|
396
400
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
+
// Kein Context = keine RLS (z.B. System-Operationen)
|
|
402
|
+
if (!context?.userId || !context?.userRole) {
|
|
403
|
+
return query(args);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const { userId, userRole } = context;
|
|
401
407
|
|
|
402
|
-
|
|
408
|
+
// IMPORTANT: The entire operation must happen in ONE transaction
|
|
409
|
+
// We need to wrap the ENTIRE query execution in a single transaction
|
|
403
410
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
411
|
+
// For operations that are already transactions, just set the variables
|
|
412
|
+
if (operation === '$transaction') {
|
|
413
|
+
return basePrisma.$transaction(async (tx) => {
|
|
414
|
+
await setRLSVariables(tx, userId, userRole);
|
|
415
|
+
return query(args);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
408
418
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
419
|
+
// For regular operations, wrap in transaction with RLS
|
|
420
|
+
return basePrisma.$transaction(async (tx) => {
|
|
421
|
+
// Set session variables
|
|
422
|
+
await setRLSVariables(tx, userId, userRole);
|
|
423
|
+
|
|
424
|
+
// Execute the original query using the transaction client
|
|
425
|
+
// This is the key: we need to use the transaction client for the query
|
|
426
|
+
if (model) {
|
|
427
|
+
// Model query (e.g., user.findMany())
|
|
428
|
+
return tx[model][operation](args);
|
|
429
|
+
} else {
|
|
430
|
+
// Raw query or special operation
|
|
431
|
+
return tx[operation](args);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
},
|
|
412
435
|
},
|
|
413
|
-
},
|
|
414
436
|
});
|
|
415
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
|
+
|
|
452
|
+
// Alternative approach: Manual transaction wrapper
|
|
453
|
+
class PrismaWithRLS {
|
|
454
|
+
constructor() {
|
|
455
|
+
this.client = basePrisma;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Execute any Prisma operation with RLS context
|
|
460
|
+
*/
|
|
461
|
+
async withRLS(userId, userRole, callback) {
|
|
462
|
+
return this.client.$transaction(async (tx) => {
|
|
463
|
+
// Execute SET commands separately to avoid prepared statement error
|
|
464
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_id = '\${userId}'\`);
|
|
465
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_role = '\${userRole}'\`);
|
|
466
|
+
|
|
467
|
+
// Execute callback with transaction client
|
|
468
|
+
return callback(tx);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get a proxy client for a specific user
|
|
474
|
+
* This wraps ALL operations in RLS context
|
|
475
|
+
*/
|
|
476
|
+
forUser(userId, userRole) {
|
|
477
|
+
const withRLS = this.withRLS.bind(this);
|
|
478
|
+
const client = this.client;
|
|
479
|
+
|
|
480
|
+
return new Proxy({}, {
|
|
481
|
+
get(target, model) {
|
|
482
|
+
// Return a proxy for the model
|
|
483
|
+
return new Proxy({}, {
|
|
484
|
+
get(modelTarget, operation) {
|
|
485
|
+
// Return a function that wraps the operation
|
|
486
|
+
return async (args) => {
|
|
487
|
+
return withRLS(userId, userRole, async (tx) => {
|
|
488
|
+
return tx[model][operation](args);
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const prismaWithRLS = new PrismaWithRLS();
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Express Middleware: Set RLS context from authenticated user
|
|
502
|
+
*/
|
|
503
|
+
function setRLSContext(req, res, next) {
|
|
504
|
+
if (req.user) {
|
|
505
|
+
// Set context for async operations
|
|
506
|
+
requestContext.run(
|
|
507
|
+
{
|
|
508
|
+
userId: req.user.id,
|
|
509
|
+
userRole: req.user.role
|
|
510
|
+
},
|
|
511
|
+
() => next()
|
|
512
|
+
);
|
|
513
|
+
} else {
|
|
514
|
+
next();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
416
518
|
/**
|
|
417
519
|
* Helper: System-Operationen ohne RLS (für Cron-Jobs, etc.)
|
|
418
520
|
*/
|
|
419
521
|
async function withSystemAccess(callback) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
callback
|
|
423
|
-
);
|
|
522
|
+
// For system access, we might not want RLS at all
|
|
523
|
+
// So we use the base client directly
|
|
524
|
+
return callback(basePrisma);
|
|
424
525
|
}
|
|
425
526
|
|
|
426
527
|
/**
|
|
427
528
|
* Helper: Als bestimmter User ausführen (für Tests)
|
|
428
529
|
*/
|
|
429
530
|
async function withUser(userId, userRole, callback) {
|
|
430
|
-
|
|
531
|
+
return requestContext.run({ userId, userRole }, () => callback());
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Helper: Direct transaction with RLS for complex operations
|
|
536
|
+
*/
|
|
537
|
+
async function transactionWithRLS(userId, userRole, callback) {
|
|
538
|
+
return basePrisma.$transaction(async (tx) => {
|
|
539
|
+
// Set RLS context for this transaction - execute separately
|
|
540
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_id = '\${userId}'\`);
|
|
541
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_role = '\${userRole}'\`);
|
|
542
|
+
|
|
543
|
+
// Execute callback with transaction client
|
|
544
|
+
return callback(tx);
|
|
545
|
+
});
|
|
431
546
|
}
|
|
432
547
|
|
|
433
548
|
/**
|
|
434
549
|
* Helper: Hole RLS Config (für SQL Generation)
|
|
435
550
|
*/
|
|
436
551
|
function getRLSConfig() {
|
|
437
|
-
|
|
552
|
+
return RLS_CONFIG;
|
|
438
553
|
}
|
|
439
554
|
|
|
555
|
+
// Example usage in route
|
|
556
|
+
/*
|
|
557
|
+
app.get('/api/users', authenticateUser, setRLSContext, async (req, res) => {
|
|
558
|
+
// Option 1: Using extended prisma (automatic RLS)
|
|
559
|
+
const users = await prisma.user.findMany();
|
|
560
|
+
|
|
561
|
+
// Option 2: Using manual transaction
|
|
562
|
+
const users = await transactionWithRLS(req.user.id, req.user.role, async (tx) => {
|
|
563
|
+
return tx.user.findMany();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Option 3: Using forUser helper
|
|
567
|
+
const userPrisma = prismaWithRLS.forUser(req.user.id, req.user.role);
|
|
568
|
+
const users = await userPrisma.user.findMany();
|
|
569
|
+
|
|
570
|
+
res.json(users);
|
|
571
|
+
});
|
|
572
|
+
*/
|
|
573
|
+
|
|
574
|
+
module.exports = {
|
|
575
|
+
prisma,
|
|
576
|
+
prismaTransaction,
|
|
577
|
+
basePrisma, // Export base for auth operations that don't need RLS
|
|
578
|
+
PrismaClient,
|
|
579
|
+
requestContext,
|
|
580
|
+
setRLSContext,
|
|
581
|
+
withSystemAccess,
|
|
582
|
+
withUser,
|
|
583
|
+
transactionWithRLS,
|
|
584
|
+
prismaWithRLS,
|
|
585
|
+
getRLSConfig,
|
|
586
|
+
setRLSVariables,
|
|
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
|
+
|
|
440
600
|
module.exports = {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
withSystemAccess,
|
|
445
|
-
withUser,
|
|
446
|
-
getRLSConfig,
|
|
447
|
-
setRLSVariables,
|
|
448
|
-
rls
|
|
601
|
+
prisma,
|
|
602
|
+
PrismaClient,
|
|
603
|
+
acl
|
|
449
604
|
};
|
|
450
605
|
`;
|
|
606
|
+
}
|
|
451
607
|
|
|
452
608
|
// Ensure rapidd directory exists
|
|
453
609
|
const rapiddDir = path.dirname(rapiddJsPath);
|
|
@@ -514,14 +670,14 @@ async function updateRelationshipsForModel(filteredModels, relationshipsPath, pr
|
|
|
514
670
|
}
|
|
515
671
|
|
|
516
672
|
/**
|
|
517
|
-
* Update
|
|
673
|
+
* Update acl.js for a specific model
|
|
518
674
|
*/
|
|
519
|
-
async function
|
|
520
|
-
const {
|
|
675
|
+
async function updateACLForModel(filteredModels, allModels, aclPath, datasource, userTable, relationships, debug = false) {
|
|
676
|
+
const { generateACL } = require('../generators/aclGenerator');
|
|
521
677
|
|
|
522
|
-
// Generate
|
|
523
|
-
const tempPath =
|
|
524
|
-
await
|
|
678
|
+
// Generate ACL for the filtered model (but pass all models for user table detection)
|
|
679
|
+
const tempPath = aclPath + '.tmp';
|
|
680
|
+
await generateACL(
|
|
525
681
|
filteredModels,
|
|
526
682
|
tempPath,
|
|
527
683
|
datasource.url,
|
|
@@ -532,11 +688,11 @@ async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource,
|
|
|
532
688
|
allModels
|
|
533
689
|
);
|
|
534
690
|
|
|
535
|
-
// Read the generated
|
|
691
|
+
// Read the generated ACL for the specific model
|
|
536
692
|
const tempContent = fs.readFileSync(tempPath, 'utf8');
|
|
537
693
|
fs.unlinkSync(tempPath);
|
|
538
694
|
|
|
539
|
-
// Extract the model's
|
|
695
|
+
// Extract the model's ACL configuration
|
|
540
696
|
const modelName = Object.keys(filteredModels)[0];
|
|
541
697
|
|
|
542
698
|
// Find the start of the model definition
|
|
@@ -573,44 +729,43 @@ async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource,
|
|
|
573
729
|
|
|
574
730
|
if (braceCount === 0) {
|
|
575
731
|
// Found the closing brace
|
|
576
|
-
const modelRls = tempContent.substring(modelStart, i + 1);
|
|
577
732
|
break;
|
|
578
733
|
}
|
|
579
734
|
}
|
|
580
735
|
}
|
|
581
736
|
|
|
582
737
|
if (braceCount !== 0) {
|
|
583
|
-
throw new Error(`Could not extract
|
|
738
|
+
throw new Error(`Could not extract ACL for model ${modelName} - unmatched braces`);
|
|
584
739
|
}
|
|
585
740
|
|
|
586
|
-
const
|
|
741
|
+
const modelAcl = tempContent.substring(modelStart, i + 1);
|
|
587
742
|
|
|
588
|
-
// Read existing
|
|
589
|
-
if (fs.existsSync(
|
|
590
|
-
let existingContent = fs.readFileSync(
|
|
743
|
+
// Read existing acl.js
|
|
744
|
+
if (fs.existsSync(aclPath)) {
|
|
745
|
+
let existingContent = fs.readFileSync(aclPath, 'utf8');
|
|
591
746
|
|
|
592
|
-
// Check if model already exists in
|
|
747
|
+
// Check if model already exists in ACL
|
|
593
748
|
const existingModelPattern = new RegExp(`${modelName}:\\s*\\{[\\s\\S]*?\\n \\}(?=,|\\n)`);
|
|
594
749
|
|
|
595
750
|
if (existingModelPattern.test(existingContent)) {
|
|
596
|
-
// Replace existing model
|
|
597
|
-
existingContent = existingContent.replace(existingModelPattern,
|
|
751
|
+
// Replace existing model ACL
|
|
752
|
+
existingContent = existingContent.replace(existingModelPattern, modelAcl);
|
|
598
753
|
} else {
|
|
599
|
-
// Add new model
|
|
754
|
+
// Add new model ACL before the closing of acl.model
|
|
600
755
|
// Find the last closing brace of a model object and add comma after it
|
|
601
756
|
existingContent = existingContent.replace(
|
|
602
757
|
/(\n \})\n(\};)/,
|
|
603
|
-
`$1,\n ${
|
|
758
|
+
`$1,\n ${modelAcl}\n$2`
|
|
604
759
|
);
|
|
605
760
|
}
|
|
606
761
|
|
|
607
|
-
fs.writeFileSync(
|
|
762
|
+
fs.writeFileSync(aclPath, existingContent);
|
|
608
763
|
console.log(`✓ Updated RLS for model: ${modelName}`);
|
|
609
764
|
} else {
|
|
610
|
-
// If
|
|
611
|
-
await
|
|
765
|
+
// If acl.js doesn't exist, create it with just this model
|
|
766
|
+
await generateACL(
|
|
612
767
|
filteredModels,
|
|
613
|
-
|
|
768
|
+
aclPath,
|
|
614
769
|
datasource.url,
|
|
615
770
|
datasource.isPostgreSQL,
|
|
616
771
|
userTable,
|
|
@@ -642,7 +797,7 @@ async function buildModels(options) {
|
|
|
642
797
|
const modelJsPath = path.join(srcDir, 'Model.js');
|
|
643
798
|
const rapiddDir = path.join(baseDir, 'rapidd');
|
|
644
799
|
const relationshipsPath = path.join(rapiddDir, 'relationships.json');
|
|
645
|
-
const
|
|
800
|
+
const aclPath = path.join(rapiddDir, 'acl.js');
|
|
646
801
|
const rapiddJsPath = path.join(rapiddDir, 'rapidd.js');
|
|
647
802
|
const routesDir = path.join(baseDir, 'routes', 'api', 'v1');
|
|
648
803
|
const logsDir = path.join(baseDir, 'logs');
|
|
@@ -718,13 +873,13 @@ async function buildModels(options) {
|
|
|
718
873
|
const shouldGenerate = {
|
|
719
874
|
model: !options.only || options.only === 'model',
|
|
720
875
|
route: !options.only || options.only === 'route',
|
|
721
|
-
|
|
876
|
+
acl: !options.only || options.only === 'acl',
|
|
722
877
|
relationship: !options.only || options.only === 'relationship'
|
|
723
878
|
};
|
|
724
879
|
|
|
725
880
|
// Validate --only option
|
|
726
|
-
if (options.only && !['model', 'route', '
|
|
727
|
-
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`);
|
|
728
883
|
}
|
|
729
884
|
|
|
730
885
|
// Generate model files
|
|
@@ -738,10 +893,18 @@ async function buildModels(options) {
|
|
|
738
893
|
generateBaseModelFile(modelJsPath);
|
|
739
894
|
}
|
|
740
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
|
+
|
|
741
904
|
// Generate rapidd/rapidd.js if it doesn't exist
|
|
742
905
|
if (!fs.existsSync(rapiddJsPath)) {
|
|
743
906
|
console.log('Generating rapidd/rapidd.js...');
|
|
744
|
-
generateRapiddFile(rapiddJsPath);
|
|
907
|
+
generateRapiddFile(rapiddJsPath, datasource.isPostgreSQL);
|
|
745
908
|
}
|
|
746
909
|
|
|
747
910
|
// Generate relationships.json
|
|
@@ -767,9 +930,9 @@ async function buildModels(options) {
|
|
|
767
930
|
}
|
|
768
931
|
}
|
|
769
932
|
|
|
770
|
-
// Generate
|
|
771
|
-
if (shouldGenerate.
|
|
772
|
-
console.log(`\nGenerating
|
|
933
|
+
// Generate ACL configuration
|
|
934
|
+
if (shouldGenerate.acl) {
|
|
935
|
+
console.log(`\nGenerating ACL configuration...`);
|
|
773
936
|
|
|
774
937
|
// Load relationships for Prisma filter building
|
|
775
938
|
let relationships = {};
|
|
@@ -782,21 +945,19 @@ async function buildModels(options) {
|
|
|
782
945
|
}
|
|
783
946
|
|
|
784
947
|
try {
|
|
785
|
-
// Parse datasource from Prisma schema to get database URL
|
|
786
|
-
const datasource = parseDatasource(schemaPath);
|
|
787
948
|
|
|
788
|
-
// For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive
|
|
949
|
+
// For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive ACL
|
|
789
950
|
if (!datasource.isPostgreSQL) {
|
|
790
|
-
console.log(`${datasource.provider || 'Non-PostgreSQL'} database detected - generating permissive
|
|
791
|
-
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);
|
|
792
953
|
} else if (options.model) {
|
|
793
|
-
// Update only specific model in
|
|
794
|
-
await
|
|
954
|
+
// Update only specific model in acl.js
|
|
955
|
+
await updateACLForModel(filteredModels, models, aclPath, datasource, options.userTable, relationships, options.debug);
|
|
795
956
|
} else {
|
|
796
|
-
// Generate
|
|
797
|
-
await
|
|
957
|
+
// Generate ACL for all models
|
|
958
|
+
await generateACL(
|
|
798
959
|
models,
|
|
799
|
-
|
|
960
|
+
aclPath,
|
|
800
961
|
datasource.url,
|
|
801
962
|
datasource.isPostgreSQL,
|
|
802
963
|
options.userTable,
|
|
@@ -805,10 +966,10 @@ async function buildModels(options) {
|
|
|
805
966
|
);
|
|
806
967
|
}
|
|
807
968
|
} catch (error) {
|
|
808
|
-
console.error('Failed to generate
|
|
809
|
-
console.log('Generating permissive
|
|
969
|
+
console.error('Failed to generate ACL:', error.message);
|
|
970
|
+
console.log('Generating permissive ACL fallback...');
|
|
810
971
|
// Pass null for URL and false for isPostgreSQL to skip database connection
|
|
811
|
-
await
|
|
972
|
+
await generateACL(models, aclPath, null, false, options.userTable, relationships, options.debug);
|
|
812
973
|
}
|
|
813
974
|
}
|
|
814
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
|
-
};
|