@rapidd/build 1.0.5 → 1.0.7
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/package.json +1 -1
- package/src/commands/build.js +166 -44
- package/src/generators/routeGenerator.js +3 -4
package/package.json
CHANGED
package/src/commands/build.js
CHANGED
|
@@ -12,7 +12,7 @@ const { generateAllRoutes } = require('../generators/routeGenerator');
|
|
|
12
12
|
*/
|
|
13
13
|
function generateBaseModelFile(modelJsPath) {
|
|
14
14
|
const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
|
|
15
|
-
const {rls} = require('
|
|
15
|
+
const {rls} = require('../rapidd/rapidd');
|
|
16
16
|
const {ErrorResponse} = require('./Api');
|
|
17
17
|
|
|
18
18
|
class Model {
|
|
@@ -33,11 +33,11 @@ class Model {
|
|
|
33
33
|
_filter = (q) => this.queryBuilder.filter(q);
|
|
34
34
|
_include = (include) => this.queryBuilder.include(include, this.user);
|
|
35
35
|
// RLS METHODS
|
|
36
|
-
_canCreate = () => this.rls
|
|
37
|
-
_hasAccess = (data) => this.rls
|
|
38
|
-
_getAccessFilter = () => this.rls
|
|
39
|
-
_getUpdateFilter = () => this.rls
|
|
40
|
-
_getDeleteFilter = () => this.rls
|
|
36
|
+
_canCreate = () => this.rls.canCreate(this.user);
|
|
37
|
+
_hasAccess = (data) => this.rls.hasAccess?.(data, this.user) || false;
|
|
38
|
+
_getAccessFilter = () => this.rls.getAccessFilter?.(this.user);
|
|
39
|
+
_getUpdateFilter = () => this.rls.getUpdateFilter(this.user);
|
|
40
|
+
_getDeleteFilter = () => this.rls.getDeleteFilter(this.user);
|
|
41
41
|
_omit = () => this.queryBuilder.omit(this.user);
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -366,86 +366,208 @@ const requestContext = new AsyncLocalStorage();
|
|
|
366
366
|
|
|
367
367
|
// RLS Configuration aus Environment Variables
|
|
368
368
|
const RLS_CONFIG = {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
namespace: process.env.RLS_NAMESPACE || 'app',
|
|
370
|
+
userId: process.env.RLS_USER_ID || 'current_user_id',
|
|
371
|
+
userRole: process.env.RLS_USER_ROLE || 'current_user_role',
|
|
372
372
|
};
|
|
373
373
|
|
|
374
374
|
// Basis Prisma Client
|
|
375
375
|
const basePrisma = new PrismaClient({
|
|
376
|
-
|
|
376
|
+
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
377
377
|
});
|
|
378
378
|
|
|
379
379
|
/**
|
|
380
|
-
* Setze RLS Session Variables in PostgreSQL
|
|
380
|
+
* FIXED: Setze RLS Session Variables in PostgreSQL
|
|
381
|
+
* Execute each SET command separately to avoid prepared statement error
|
|
381
382
|
*/
|
|
382
383
|
async function setRLSVariables(tx, userId, userRole) {
|
|
383
384
|
const namespace = RLS_CONFIG.namespace;
|
|
384
385
|
const userIdVar = RLS_CONFIG.userId;
|
|
385
386
|
const userRoleVar = RLS_CONFIG.userRole;
|
|
386
387
|
|
|
387
|
-
|
|
388
|
-
await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}
|
|
388
|
+
// Execute SET commands separately (PostgreSQL doesn't allow multiple commands in prepared statements)
|
|
389
|
+
await tx.$executeRawUnsafe(\`SET LOCAL "\${namespace}"."\${userIdVar}" = '\${userId}'\`);
|
|
390
|
+
await tx.$executeRawUnsafe(\`SET LOCAL "\${namespace}"."\${userRoleVar}" = '\${userRole}'\`);
|
|
389
391
|
}
|
|
390
392
|
|
|
391
|
-
// Erweiterter Prisma mit automatischer RLS
|
|
393
|
+
// FIXED: Erweiterter Prisma mit automatischer RLS
|
|
392
394
|
const prisma = basePrisma.$extends({
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
395
|
+
query: {
|
|
396
|
+
async $allOperations({ operation, args, query, model }) {
|
|
397
|
+
const context = requestContext.getStore();
|
|
396
398
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
// Kein Context = keine RLS (z.B. System-Operationen)
|
|
400
|
+
if (!context?.userId || !context?.userRole) {
|
|
401
|
+
return query(args);
|
|
402
|
+
}
|
|
401
403
|
|
|
402
|
-
|
|
404
|
+
const { userId, userRole } = context;
|
|
403
405
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// Session-Variablen setzen
|
|
407
|
-
await setRLSVariables(tx, userId, userRole);
|
|
406
|
+
// IMPORTANT: The entire operation must happen in ONE transaction
|
|
407
|
+
// We need to wrap the ENTIRE query execution in a single transaction
|
|
408
408
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
409
|
+
// For operations that are already transactions, just set the variables
|
|
410
|
+
if (operation === '$transaction') {
|
|
411
|
+
return basePrisma.$transaction(async (tx) => {
|
|
412
|
+
await setRLSVariables(tx, userId, userRole);
|
|
413
|
+
return query(args);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// For regular operations, wrap in transaction with RLS
|
|
418
|
+
return basePrisma.$transaction(async (tx) => {
|
|
419
|
+
// Set session variables
|
|
420
|
+
await setRLSVariables(tx, userId, userRole);
|
|
421
|
+
|
|
422
|
+
// Execute the original query using the transaction client
|
|
423
|
+
// This is the key: we need to use the transaction client for the query
|
|
424
|
+
if (model) {
|
|
425
|
+
// Model query (e.g., user.findMany())
|
|
426
|
+
return tx[model][operation](args);
|
|
427
|
+
} else {
|
|
428
|
+
// Raw query or special operation
|
|
429
|
+
return tx[operation](args);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
},
|
|
412
433
|
},
|
|
413
|
-
},
|
|
414
434
|
});
|
|
415
435
|
|
|
436
|
+
// Alternative approach: Manual transaction wrapper
|
|
437
|
+
class PrismaWithRLS {
|
|
438
|
+
constructor() {
|
|
439
|
+
this.client = basePrisma;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Execute any Prisma operation with RLS context
|
|
444
|
+
*/
|
|
445
|
+
async withRLS(userId, userRole, callback) {
|
|
446
|
+
return this.client.$transaction(async (tx) => {
|
|
447
|
+
// Execute SET commands separately to avoid prepared statement error
|
|
448
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_id = '\${userId}'\`);
|
|
449
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_role = '\${userRole}'\`);
|
|
450
|
+
|
|
451
|
+
// Execute callback with transaction client
|
|
452
|
+
return callback(tx);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get a proxy client for a specific user
|
|
458
|
+
* This wraps ALL operations in RLS context
|
|
459
|
+
*/
|
|
460
|
+
forUser(userId, userRole) {
|
|
461
|
+
const withRLS = this.withRLS.bind(this);
|
|
462
|
+
const client = this.client;
|
|
463
|
+
|
|
464
|
+
return new Proxy({}, {
|
|
465
|
+
get(target, model) {
|
|
466
|
+
// Return a proxy for the model
|
|
467
|
+
return new Proxy({}, {
|
|
468
|
+
get(modelTarget, operation) {
|
|
469
|
+
// Return a function that wraps the operation
|
|
470
|
+
return async (args) => {
|
|
471
|
+
return withRLS(userId, userRole, async (tx) => {
|
|
472
|
+
return tx[model][operation](args);
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const prismaWithRLS = new PrismaWithRLS();
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Express Middleware: Set RLS context from authenticated user
|
|
486
|
+
*/
|
|
487
|
+
function setRLSContext(req, res, next) {
|
|
488
|
+
if (req.user) {
|
|
489
|
+
// Set context for async operations
|
|
490
|
+
requestContext.run(
|
|
491
|
+
{
|
|
492
|
+
userId: req.user.id,
|
|
493
|
+
userRole: req.user.role
|
|
494
|
+
},
|
|
495
|
+
() => next()
|
|
496
|
+
);
|
|
497
|
+
} else {
|
|
498
|
+
next();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
416
502
|
/**
|
|
417
503
|
* Helper: System-Operationen ohne RLS (für Cron-Jobs, etc.)
|
|
418
504
|
*/
|
|
419
505
|
async function withSystemAccess(callback) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
callback
|
|
423
|
-
);
|
|
506
|
+
// For system access, we might not want RLS at all
|
|
507
|
+
// So we use the base client directly
|
|
508
|
+
return callback(basePrisma);
|
|
424
509
|
}
|
|
425
510
|
|
|
426
511
|
/**
|
|
427
512
|
* Helper: Als bestimmter User ausführen (für Tests)
|
|
428
513
|
*/
|
|
429
514
|
async function withUser(userId, userRole, callback) {
|
|
430
|
-
|
|
515
|
+
return requestContext.run({ userId, userRole }, () => callback());
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Helper: Direct transaction with RLS for complex operations
|
|
520
|
+
*/
|
|
521
|
+
async function transactionWithRLS(userId, userRole, callback) {
|
|
522
|
+
return basePrisma.$transaction(async (tx) => {
|
|
523
|
+
// Set RLS context for this transaction - execute separately
|
|
524
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_id = '\${userId}'\`);
|
|
525
|
+
await tx.$executeRawUnsafe(\`SET LOCAL app.current_user_role = '\${userRole}'\`);
|
|
526
|
+
|
|
527
|
+
// Execute callback with transaction client
|
|
528
|
+
return callback(tx);
|
|
529
|
+
});
|
|
431
530
|
}
|
|
432
531
|
|
|
433
532
|
/**
|
|
434
533
|
* Helper: Hole RLS Config (für SQL Generation)
|
|
435
534
|
*/
|
|
436
535
|
function getRLSConfig() {
|
|
437
|
-
|
|
536
|
+
return RLS_CONFIG;
|
|
438
537
|
}
|
|
439
538
|
|
|
539
|
+
// Example usage in route
|
|
540
|
+
/*
|
|
541
|
+
app.get('/api/users', authenticateUser, setRLSContext, async (req, res) => {
|
|
542
|
+
// Option 1: Using extended prisma (automatic RLS)
|
|
543
|
+
const users = await prisma.user.findMany();
|
|
544
|
+
|
|
545
|
+
// Option 2: Using manual transaction
|
|
546
|
+
const users = await transactionWithRLS(req.user.id, req.user.role, async (tx) => {
|
|
547
|
+
return tx.user.findMany();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Option 3: Using forUser helper
|
|
551
|
+
const userPrisma = prismaWithRLS.forUser(req.user.id, req.user.role);
|
|
552
|
+
const users = await userPrisma.user.findMany();
|
|
553
|
+
|
|
554
|
+
res.json(users);
|
|
555
|
+
});
|
|
556
|
+
*/
|
|
557
|
+
|
|
440
558
|
module.exports = {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
559
|
+
prisma,
|
|
560
|
+
basePrisma, // Export base for auth operations that don't need RLS
|
|
561
|
+
PrismaClient,
|
|
562
|
+
requestContext,
|
|
563
|
+
setRLSContext,
|
|
564
|
+
withSystemAccess,
|
|
565
|
+
withUser,
|
|
566
|
+
transactionWithRLS,
|
|
567
|
+
prismaWithRLS,
|
|
568
|
+
getRLSConfig,
|
|
569
|
+
setRLSVariables,
|
|
570
|
+
rls
|
|
449
571
|
};
|
|
450
572
|
`;
|
|
451
573
|
|
|
@@ -13,12 +13,12 @@ function generateRouteFile(modelName) {
|
|
|
13
13
|
const {${className}, QueryBuilder, prisma} = require('../../../src/Model/${className}');
|
|
14
14
|
|
|
15
15
|
router.all('*', async (req, res, next) => {
|
|
16
|
-
if(req.
|
|
16
|
+
if(req.user){
|
|
17
17
|
req.${className} = new ${className}({'user': req.user});
|
|
18
18
|
next();
|
|
19
19
|
}
|
|
20
20
|
else{
|
|
21
|
-
return res.sendError(401,
|
|
21
|
+
return res.sendError(401, "no_valid_session");
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -82,8 +82,7 @@ router.patch('/:id', async function(req, res) {
|
|
|
82
82
|
router.delete('/:id', async (req, res)=>{
|
|
83
83
|
try{
|
|
84
84
|
await req.${className}.delete(req.params.id);
|
|
85
|
-
|
|
86
|
-
return res.sendResponse(200, message);
|
|
85
|
+
return res.sendResponse(200, "object_deleted_successfully", {modelName: "${className}"});
|
|
87
86
|
}
|
|
88
87
|
catch(error){
|
|
89
88
|
const response = QueryBuilder.errorHandler(error);
|