@rapidd/build 1.1.2 → 1.1.3

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.1.2",
3
+ "version": "1.1.3",
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": {
@@ -29,9 +29,16 @@ class Model {
29
29
  this.user_id = this.user ? this.user.id : null;
30
30
  }
31
31
 
32
+ get primaryKey(){
33
+ const pkey = this.queryBuilder.getPrimaryKey();
34
+ return Array.isArray(pkey) ? pkey.join('_') : pkey;
35
+ }
36
+
32
37
  _select = (fields) => this.queryBuilder.select(fields);
33
38
  _filter = (q) => this.queryBuilder.filter(q);
34
39
  _include = (include) => this.queryBuilder.include(include, this.user);
40
+ _queryCreate = (data) => this.queryBuilder.create(data, this.user);
41
+ _queryUpdate = (id, data) => this.queryBuilder.update(id, data, this.user);
35
42
  // ACL METHODS
36
43
  _canCreate = () => this.acl.canCreate(this.user);
37
44
  _getAccessFilter = () => this.acl.getAccessFilter?.(this.user);
@@ -49,16 +56,16 @@ class Model {
49
56
  * @param {'asc'|'desc'} sortOrder
50
57
  * @returns {Promise<Object[]>}
51
58
  */
52
- _getMany = async (q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc", options = {})=>{
59
+ _getMany = async (q = {}, include = "", limit = 25, offset = 0, sortBy = this.primaryKey, sortOrder = "asc", options = {})=>{
53
60
  const take = this.take(Number(limit));
54
61
  const skip = this.skip(Number(offset));
55
62
 
56
- sortBy = sortBy.trim();
57
- sortOrder = sortOrder.trim();
63
+ sortBy = sortBy?.trim();
64
+ sortOrder = sortOrder?.trim();
58
65
  if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
59
66
  throw new ErrorResponse(400, "invalid_sort_field", {sortBy, modelName: this.constructor.name});
60
67
  }
61
-
68
+
62
69
  // Query the database using Prisma with filters, pagination, and limits
63
70
  const [data, total] = await prismaTransaction([
64
71
  (tx) => tx[this.name].findMany({
@@ -83,11 +90,11 @@ class Model {
83
90
  */
84
91
  _get = async (id, include, options = {}) =>{
85
92
  const {omit, ..._options} = options;
86
- id = Number(id);
93
+ console.log(JSON.stringify(this.include(include)));
87
94
  // To determine if the record is inaccessible, either due to non-existence or insufficient permissions, two simultaneous queries are performed.
88
95
  const _response = this.prisma.findUnique({
89
96
  'where': {
90
- 'id': id
97
+ [this.primaryKey]: id,
91
98
  },
92
99
  'include': this.include(include),
93
100
  'omit': {...this._omit(), ...omit},
@@ -96,7 +103,7 @@ class Model {
96
103
 
97
104
  const _checkPermission = this.prisma.findUnique({
98
105
  'where': {
99
- 'id': id,
106
+ [this.primaryKey]: id,
100
107
  ...this.getAccessFilter()
101
108
  },
102
109
  'select': {
@@ -107,16 +114,16 @@ class Model {
107
114
  const [response, checkPermission] = await Promise.all([_response, _checkPermission]);
108
115
  if(response){
109
116
  if(checkPermission){
110
- if(response.id != checkExistence?.id){ // IN CASE access_filter CONTAINS id FIELD
111
- throw new ErrorResponse(getTranslation("no_permission"), 403);
117
+ if(response.id != checkPermission?.id){ // IN CASE access_filter CONTAINS id FIELD
118
+ throw new ErrorResponse(403, "no_permission");
112
119
  }
113
120
  }
114
121
  else{
115
- throw new ErrorResponse(getTranslation("no_permission"), 403);
122
+ throw new ErrorResponse(403, "no_permission");
116
123
  }
117
124
  }
118
125
  else{
119
- throw new ErrorResponse(getTranslation("record_not_found"), 404);
126
+ throw new ErrorResponse(404, "record_not_found");
120
127
  }
121
128
  return response;
122
129
  }
@@ -131,7 +138,7 @@ class Model {
131
138
  }
132
139
 
133
140
  // VALIDATE PASSED FIELDS AND RELATIONSHIPS
134
- this.queryBuilder.create(data, this.user_id);
141
+ this._queryCreate(data);
135
142
 
136
143
  // CREATE
137
144
  return await this.prisma.create({
@@ -147,8 +154,8 @@ class Model {
147
154
  * @returns {Promise<Object>}
148
155
  */
149
156
  _update = async (id, data, options = {}) => {
150
- id = Number(id);
151
-
157
+ delete data.createdAt;
158
+ delete data.createdBy;
152
159
  // CHECK UPDATE PERMISSION
153
160
  const updateFilter = this.getUpdateFilter();
154
161
  if (updateFilter === false) {
@@ -156,10 +163,10 @@ class Model {
156
163
  }
157
164
 
158
165
  // VALIDATE PASSED FIELDS AND RELATIONSHIPS
159
- this.queryBuilder.update(id, data, this.user_id);
166
+ this._queryUpdate(id, data);
160
167
  const response = await this.prisma.update({
161
168
  'where': {
162
- 'id': id,
169
+ [this.primaryKey]: id,
163
170
  ...updateFilter
164
171
  },
165
172
  'data': data,
@@ -188,8 +195,6 @@ class Model {
188
195
  * @returns {Promise<Object>}
189
196
  */
190
197
  _delete = async (id, options = {}) => {
191
- id = Number(id);
192
-
193
198
  // CHECK DELETE PERMISSION
194
199
  const deleteFilter = this.getDeleteFilter();
195
200
  if (deleteFilter === false) {
@@ -198,7 +203,7 @@ class Model {
198
203
 
199
204
  const response = await this.prisma.delete({
200
205
  'where': {
201
- id: parseInt(id),
206
+ [this.primaryKey]: id,
202
207
  ...deleteFilter
203
208
  },
204
209
  'select': this.select(),
@@ -229,7 +234,7 @@ class Model {
229
234
  * @returns {Promise<{} | null>}
230
235
  */
231
236
  async get(id, include, options = {}){
232
- return await this._get(Number(id), include, options);
237
+ return await this._get(id, include, options);
233
238
  }
234
239
 
235
240
  /**
@@ -238,7 +243,7 @@ class Model {
238
243
  * @returns {Promise<Object>}
239
244
  */
240
245
  async update(id, data, options = {}){
241
- return await this._update(Number(id), data, options);
246
+ return await this._update(id, data, options);
242
247
  }
243
248
 
244
249
  /**
@@ -255,7 +260,7 @@ class Model {
255
260
  * @returns {Promise<Object>}
256
261
  */
257
262
  async delete(id, data, options = {}){
258
- return await this._delete(Number(id), data, options);
263
+ return await this._delete(id, data, options);
259
264
  }
260
265
 
261
266
  select(fields){
@@ -359,7 +364,7 @@ function generateRapiddFile(rapiddJsPath, isPostgreSQL = true) {
359
364
 
360
365
  if (isPostgreSQL) {
361
366
  // PostgreSQL version with RLS support
362
- content = `const { PrismaClient } = require('../prisma/client');
367
+ content = `const { PrismaClient, Prisma } = require('../prisma/client');
363
368
  const { AsyncLocalStorage } = require('async_hooks');
364
369
  const acl = require('./acl');
365
370
 
@@ -373,13 +378,49 @@ const RLS_CONFIG = {
373
378
  userRole: process.env.RLS_USER_ROLE || 'current_user_role',
374
379
  };
375
380
 
376
- // Basis Prisma Client
381
+ // =====================================================
382
+ // BASE PRISMA CLIENTS
383
+ // =====================================================
384
+
385
+ /**
386
+ * ADMIN CLIENT - Bypasses ALL RLS
387
+ * Uses DATABASE_URL_ADMIN connection (e.g., app_auth_proxy user)
388
+ * Use ONLY for authentication operations:
389
+ * - Login
390
+ * - Register
391
+ * - Email Verification
392
+ * - Password Reset
393
+ * - OAuth operations
394
+ */
395
+ const authPrisma = new PrismaClient({
396
+ datasources: {
397
+ db: {
398
+ url: process.env.DATABASE_URL_ADMIN
399
+ }
400
+ },
401
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
402
+ });
403
+
404
+ /**
405
+ * BASE CLIENT - Regular user with RLS
406
+ * Uses DATABASE_URL connection
407
+ * Use for all business operations
408
+ */
377
409
  const basePrisma = new PrismaClient({
410
+ datasources: {
411
+ db: {
412
+ url: process.env.DATABASE_URL
413
+ }
414
+ },
378
415
  log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
379
416
  });
380
417
 
418
+ // =====================================================
419
+ // RLS HELPER FUNCTIONS
420
+ // =====================================================
421
+
381
422
  /**
382
- * FIXED: Setze RLS Session Variables in PostgreSQL
423
+ * Set RLS Session Variables in PostgreSQL
383
424
  * Execute each SET command separately to avoid prepared statement error
384
425
  */
385
426
  async function setRLSVariables(tx, userId, userRole) {
@@ -387,27 +428,48 @@ async function setRLSVariables(tx, userId, userRole) {
387
428
  const userIdVar = RLS_CONFIG.userId;
388
429
  const userRoleVar = RLS_CONFIG.userRole;
389
430
 
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}'\`);
431
+ // Execute SET commands separately
432
+ await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userIdVar} = '\${userId}'\`);
433
+ await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userRoleVar} = '\${userRole}'\`);
393
434
  }
394
435
 
395
- // FIXED: Erweiterter Prisma mit automatischer RLS
436
+ /**
437
+ * Reset RLS Session Variables
438
+ */
439
+ async function resetRLSVariables(tx) {
440
+ const namespace = RLS_CONFIG.namespace;
441
+ const userIdVar = RLS_CONFIG.userId;
442
+ const userRoleVar = RLS_CONFIG.userRole;
443
+
444
+ try {
445
+ await tx.$executeRawUnsafe(\`RESET \${namespace}.\${userIdVar}\`);
446
+ await tx.$executeRawUnsafe(\`RESET \${namespace}.\${userRoleVar}\`);
447
+ } catch (e) {
448
+ // Ignore errors on reset
449
+ console.error('Failed to reset RLS variables:', e);
450
+ }
451
+ }
452
+
453
+ // =====================================================
454
+ // EXTENDED PRISMA WITH AUTOMATIC RLS
455
+ // =====================================================
456
+
457
+ /**
458
+ * Extended Prisma Client with automatic RLS context
459
+ * Automatically wraps all operations in RLS context from AsyncLocalStorage
460
+ */
396
461
  const prisma = basePrisma.$extends({
397
462
  query: {
398
463
  async $allOperations({ operation, args, query, model }) {
399
464
  const context = requestContext.getStore();
400
465
 
401
- // Kein Context = keine RLS (z.B. System-Operationen)
466
+ // No context = no RLS (e.g., system operations)
402
467
  if (!context?.userId || !context?.userRole) {
403
468
  return query(args);
404
469
  }
405
470
 
406
471
  const { userId, userRole } = context;
407
472
 
408
- // IMPORTANT: The entire operation must happen in ONE transaction
409
- // We need to wrap the ENTIRE query execution in a single transaction
410
-
411
473
  // For operations that are already transactions, just set the variables
412
474
  if (operation === '$transaction') {
413
475
  return basePrisma.$transaction(async (tx) => {
@@ -422,7 +484,6 @@ const prisma = basePrisma.$extends({
422
484
  await setRLSVariables(tx, userId, userRole);
423
485
 
424
486
  // Execute the original query using the transaction client
425
- // This is the key: we need to use the transaction client for the query
426
487
  if (model) {
427
488
  // Model query (e.g., user.findMany())
428
489
  return tx[model][operation](args);
@@ -435,70 +496,33 @@ const prisma = basePrisma.$extends({
435
496
  },
436
497
  });
437
498
 
438
- // Helper for batch operations in single transaction
499
+ // =====================================================
500
+ // TRANSACTION HELPERS
501
+ // =====================================================
502
+
503
+ /**
504
+ * Helper for batch operations in single transaction
505
+ */
439
506
  async function prismaTransaction(operations) {
440
507
  const context = requestContext.getStore();
441
-
508
+
442
509
  if (!context?.userId || !context?.userRole) {
443
510
  return Promise.all(operations);
444
511
  }
445
-
512
+
446
513
  return basePrisma.$transaction(async (tx) => {
447
514
  await setRLSVariables(tx, context.userId, context.userRole);
448
515
  return Promise.all(operations.map(op => op(tx)));
449
516
  });
450
517
  }
451
518
 
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();
519
+ // =====================================================
520
+ // CONTEXT HELPERS
521
+ // =====================================================
499
522
 
500
523
  /**
501
524
  * Express Middleware: Set RLS context from authenticated user
525
+ * Use this AFTER your authentication middleware
502
526
  */
503
527
  function setRLSContext(req, res, next) {
504
528
  if (req.user) {
@@ -516,74 +540,50 @@ function setRLSContext(req, res, next) {
516
540
  }
517
541
 
518
542
  /**
519
- * Helper: System-Operationen ohne RLS (für Cron-Jobs, etc.)
520
- */
521
- async function withSystemAccess(callback) {
522
- // For system access, we might not want RLS at all
523
- // So we use the base client directly
524
- return callback(basePrisma);
525
- }
526
-
527
- /**
528
- * Helper: Als bestimmter User ausführen (für Tests)
529
- */
530
- async function withUser(userId, userRole, callback) {
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
- });
546
- }
547
-
548
- /**
549
- * Helper: Hole RLS Config (für SQL Generation)
543
+ * Get RLS Config (for SQL generation)
550
544
  */
551
545
  function getRLSConfig() {
552
546
  return RLS_CONFIG;
553
547
  }
554
548
 
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();
549
+ // =====================================================
550
+ // GRACEFUL SHUTDOWN
551
+ // =====================================================
560
552
 
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();
553
+ async function disconnectAll() {
554
+ await authPrisma.$disconnect();
555
+ await basePrisma.$disconnect();
556
+ }
569
557
 
570
- res.json(users);
558
+ process.on('beforeExit', async () => {
559
+ await disconnectAll();
571
560
  });
572
- */
561
+
562
+ // =====================================================
563
+ // EXPORTS
564
+ // =====================================================
573
565
 
574
566
  module.exports = {
575
- prisma,
567
+ // Main clients
568
+ prisma, // Use for regular operations with automatic RLS from context
569
+ authPrisma, // Use ONLY for auth operations (login, register, etc.)
570
+
571
+ // Transaction helpers
576
572
  prismaTransaction,
577
- basePrisma, // Export base for auth operations that don't need RLS
578
- PrismaClient,
573
+
574
+ // Context helpers
579
575
  requestContext,
580
576
  setRLSContext,
581
- withSystemAccess,
582
- withUser,
583
- transactionWithRLS,
584
- prismaWithRLS,
585
- getRLSConfig,
577
+
578
+ // RLS utilities
586
579
  setRLSVariables,
580
+ resetRLSVariables,
581
+ getRLSConfig,
582
+
583
+ // Utilities
584
+ disconnectAll,
585
+ PrismaClient,
586
+ Prisma,
587
587
  acl
588
588
  };
589
589
  `;
@@ -37,7 +37,7 @@ class ${className} extends Model {
37
37
  * @returns {{} | null}
38
38
  */
39
39
  async get(id, include){
40
- return await this._get(Number(id), include);
40
+ return await this._get(id, include);
41
41
  }
42
42
 
43
43
  /**
@@ -54,7 +54,7 @@ class ${className} extends Model {
54
54
  * @returns {Object}
55
55
  */
56
56
  async update(id, data){
57
- return await this._update(Number(id), data);
57
+ return await this._update(id, data);
58
58
  }
59
59
 
60
60
  /**
@@ -62,7 +62,7 @@ class ${className} extends Model {
62
62
  * @returns {Object}
63
63
  */
64
64
  async delete(id){
65
- return await this._delete(Number(id));
65
+ return await this._delete(id);
66
66
  }
67
67
 
68
68
  /**