@rapidd/build 1.0.4 → 1.0.5

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.4",
3
+ "version": "1.0.5",
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": {
@@ -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) || false;
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
  /**
@@ -150,33 +150,21 @@ class Model {
150
150
  throw new ErrorResponse(403, "no_permission_to_update");
151
151
  }
152
152
 
153
- // GET DATA FIRST (also checks read access)
154
- const current_data = await this._get(id, "ALL");
155
-
156
- // If updateFilter is not empty, verify the record matches the filter
157
- if (Object.keys(updateFilter).length > 0) {
158
- const canUpdate = await this.prisma.findUnique({
159
- 'where': {
160
- 'id': id,
161
- ...updateFilter
162
- },
163
- 'select': { 'id': true }
164
- });
165
- if (!canUpdate) {
166
- throw new ErrorResponse(403, "no_permission_to_update");
167
- }
168
- }
169
-
170
153
  // VALIDATE PASSED FIELDS AND RELATIONSHIPS
171
154
  this.queryBuilder.update(id, data, this.user_id);
172
- return await this.prisma.update({
155
+ const response = await this.prisma.update({
173
156
  'where': {
174
- 'id': id
157
+ 'id': id,
158
+ ...updateFilter
175
159
  },
176
160
  'data': data,
177
161
  'include': this.include('ALL'),
178
162
  ...options
179
163
  });
164
+ if(response){
165
+ return response;
166
+ }
167
+ throw new ErrorResponse(403, "no_permission");
180
168
  }
181
169
 
182
170
  /**
@@ -203,30 +191,18 @@ class Model {
203
191
  throw new ErrorResponse(403, "no_permission_to_delete");
204
192
  }
205
193
 
206
- // GET DATA FIRST (also checks read access)
207
- const current_data = await this._get(id);
208
-
209
- // If deleteFilter is not empty, verify the record matches the filter
210
- if (Object.keys(deleteFilter).length > 0) {
211
- const canDelete = await this.prisma.findUnique({
212
- 'where': {
213
- 'id': id,
214
- ...deleteFilter
215
- },
216
- 'select': { 'id': true }
217
- });
218
- if (!canDelete) {
219
- throw new ErrorResponse(403, "no_permission_to_delete");
220
- }
221
- }
222
-
223
- return await this.prisma.delete({
194
+ const response = await this.prisma.delete({
224
195
  'where': {
225
- id: parseInt(id)
196
+ id: parseInt(id),
197
+ ...deleteFilter
226
198
  },
227
199
  'select': this.select(),
228
200
  ...options
229
201
  });
202
+ if(response){
203
+ return response;
204
+ }
205
+ throw new ErrorResponse(403, "no_permission");
230
206
  }
231
207
 
232
208
  /**
@@ -306,7 +282,7 @@ class Model {
306
282
  */
307
283
  getAccessFilter(){
308
284
  const filter = this._getAccessFilter()
309
- if(this.user.role == "application" || filter == true){
285
+ if(this.user.role == "application" || filter === true){
310
286
  return {};
311
287
  }
312
288
  return this._getAccessFilter();
@@ -336,7 +312,7 @@ class Model {
336
312
  */
337
313
  getUpdateFilter(){
338
314
  const filter = this._getUpdateFilter();
339
- if(this.user.role == "application" || filter == true){
315
+ if(this.user.role == "application" || filter === true){
340
316
  return {};
341
317
  }
342
318
  return filter;
@@ -348,7 +324,7 @@ class Model {
348
324
  */
349
325
  getDeleteFilter(){
350
326
  const filter = this._getDeleteFilter();
351
- if(this.user.role == "application" || filter == true){
327
+ if(this.user.role == "application" || filter === true){
352
328
  return {};
353
329
  }
354
330
  return filter;
@@ -381,12 +357,96 @@ module.exports = {Model, QueryBuilder, prisma};
381
357
  * Generate rapidd/rapidd.js file
382
358
  */
383
359
  function generateRapiddFile(rapiddJsPath) {
384
- const content = `const { PrismaClient, Prisma } = require('../prisma/client');
360
+ const content = `const { PrismaClient } = require('../prisma/client');
361
+ const { AsyncLocalStorage } = require('async_hooks');
385
362
  const rls = require('./rls');
386
363
 
387
- const prisma = new PrismaClient();
364
+ // Request Context Storage
365
+ const requestContext = new AsyncLocalStorage();
366
+
367
+ // RLS Configuration aus Environment Variables
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',
372
+ };
373
+
374
+ // Basis Prisma Client
375
+ const basePrisma = new PrismaClient({
376
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
377
+ });
378
+
379
+ /**
380
+ * Setze RLS Session Variables in PostgreSQL
381
+ */
382
+ async function setRLSVariables(tx, userId, userRole) {
383
+ const namespace = RLS_CONFIG.namespace;
384
+ const userIdVar = RLS_CONFIG.userId;
385
+ const userRoleVar = RLS_CONFIG.userRole;
386
+
387
+ await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userIdVar} = '\${userId}'\`);
388
+ await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userRoleVar} = '\${userRole}'\`);
389
+ }
390
+
391
+ // Erweiterter Prisma mit automatischer RLS
392
+ const prisma = basePrisma.$extends({
393
+ query: {
394
+ async $allOperations({ args, query }) {
395
+ const context = requestContext.getStore();
396
+
397
+ // Kein Context = keine RLS (z.B. System-Operationen)
398
+ if (!context?.userId || !context?.userRole) {
399
+ return query(args);
400
+ }
388
401
 
389
- module.exports = {prisma, Prisma, rls};
402
+ const { userId, userRole } = context;
403
+
404
+ // Query in Transaction mit RLS ausführen
405
+ return basePrisma.$transaction(async (tx) => {
406
+ // Session-Variablen setzen
407
+ await setRLSVariables(tx, userId, userRole);
408
+
409
+ // Original Query ausführen
410
+ return query(args);
411
+ });
412
+ },
413
+ },
414
+ });
415
+
416
+ /**
417
+ * Helper: System-Operationen ohne RLS (für Cron-Jobs, etc.)
418
+ */
419
+ async function withSystemAccess(callback) {
420
+ return requestContext.run(
421
+ { userId: 'system', userRole: 'ADMIN' },
422
+ callback
423
+ );
424
+ }
425
+
426
+ /**
427
+ * Helper: Als bestimmter User ausführen (für Tests)
428
+ */
429
+ async function withUser(userId, userRole, callback) {
430
+ return requestContext.run({ userId, userRole }, callback);
431
+ }
432
+
433
+ /**
434
+ * Helper: Hole RLS Config (für SQL Generation)
435
+ */
436
+ function getRLSConfig() {
437
+ return RLS_CONFIG;
438
+ }
439
+
440
+ module.exports = {
441
+ prisma,
442
+ PrismaClient,
443
+ requestContext,
444
+ withSystemAccess,
445
+ withUser,
446
+ getRLSConfig,
447
+ setRLSVariables,
448
+ rls
449
+ };
390
450
  `;
391
451
 
392
452
  // Ensure rapidd directory exists
@@ -725,7 +785,11 @@ async function buildModels(options) {
725
785
  // Parse datasource from Prisma schema to get database URL
726
786
  const datasource = parseDatasource(schemaPath);
727
787
 
728
- if (options.model) {
788
+ // For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive RLS
789
+ if (!datasource.isPostgreSQL) {
790
+ console.log(`${datasource.provider || 'Non-PostgreSQL'} database detected - generating permissive RLS...`);
791
+ await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
792
+ } else if (options.model) {
729
793
  // Update only specific model in rls.js
730
794
  await updateRLSForModel(filteredModels, models, rlsPath, datasource, options.userTable, relationships, options.debug);
731
795
  } else {
@@ -742,10 +806,9 @@ async function buildModels(options) {
742
806
  }
743
807
  } catch (error) {
744
808
  console.error('Failed to generate RLS:', error.message);
745
- if (!options.model) {
746
- console.log('Generating permissive RLS fallback...');
747
- await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
748
- }
809
+ console.log('Generating permissive RLS fallback...');
810
+ // Pass null for URL and false for isPostgreSQL to skip database connection
811
+ await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
749
812
  }
750
813
  }
751
814
 
@@ -61,10 +61,19 @@ function parseDatasource(schemaPath) {
61
61
  isPostgreSQL = url.startsWith('postgresql://') || url.startsWith('postgres://');
62
62
  }
63
63
 
64
+ // Explicitly detect MySQL to avoid false PostgreSQL detection
65
+ const isMySQL = provider === 'mysql' || (url && url.startsWith('mysql://'));
66
+
67
+ // If it's MySQL, ensure isPostgreSQL is false
68
+ if (isMySQL) {
69
+ isPostgreSQL = false;
70
+ }
71
+
64
72
  return {
65
73
  provider,
66
74
  url,
67
- isPostgreSQL
75
+ isPostgreSQL,
76
+ isMySQL
68
77
  };
69
78
  }
70
79