@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidd/build",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Dynamic code generator that transforms Prisma schemas into Express.js CRUD APIs with PostgreSQL RLS-to-JavaScript translation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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('../../rapidd/rapidd');
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?.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);
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
- 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',
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
- log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
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
- await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userIdVar} = '\${userId}'\`);
388
- await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userRoleVar} = '\${userRole}'\`);
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
- query: {
394
- async $allOperations({ args, query }) {
395
- const context = requestContext.getStore();
395
+ query: {
396
+ async $allOperations({ operation, args, query, model }) {
397
+ const context = requestContext.getStore();
396
398
 
397
- // Kein Context = keine RLS (z.B. System-Operationen)
398
- if (!context?.userId || !context?.userRole) {
399
- return query(args);
400
- }
399
+ // Kein Context = keine RLS (z.B. System-Operationen)
400
+ if (!context?.userId || !context?.userRole) {
401
+ return query(args);
402
+ }
401
403
 
402
- const { userId, userRole } = context;
404
+ const { userId, userRole } = context;
403
405
 
404
- // Query in Transaction mit RLS ausführen
405
- return basePrisma.$transaction(async (tx) => {
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
- // Original Query ausführen
410
- return query(args);
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
- return requestContext.run(
421
- { userId: 'system', userRole: 'ADMIN' },
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
- return requestContext.run({ userId, userRole }, callback);
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
- return RLS_CONFIG;
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
- prisma,
442
- PrismaClient,
443
- requestContext,
444
- withSystemAccess,
445
- withUser,
446
- getRLSConfig,
447
- setRLSVariables,
448
- rls
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.session && req.user){
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, req.getTranslation("no_valid_session"));
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
- const message = req.getTranslation("object_deleted_successfully", {modelName: "${className}"});
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);