@rapidd/build 1.0.3 → 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.3",
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": {
@@ -12,7 +12,8 @@ const { generateAllRoutes } = require('../generators/routeGenerator');
12
12
  */
13
13
  function generateBaseModelFile(modelJsPath) {
14
14
  const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
15
- const {ErrorResponse, getTranslation} = require('./Api');
15
+ const {rls} = require('../../rapidd/rapidd');
16
+ const {ErrorResponse} = require('./Api');
16
17
 
17
18
  class Model {
18
19
  /**
@@ -21,17 +22,23 @@ class Model {
21
22
  */
22
23
  constructor(name, options){
23
24
  this.modelName = name;
25
+ this.queryBuilder = new QueryBuilder(name);
26
+ this.rls = rls.model[name] || {};
24
27
  this.options = options || {}
25
28
  this.user = this.options.user || {'id': 1, 'role': 'application'};
26
29
  this.user_id = this.user ? this.user.id : null;
27
30
  }
28
31
 
29
- _select = (fields) => this.constructor.queryBuilder.select(fields);
30
- _filter = (q) => this.constructor.queryBuilder.filter(q);
31
- _include = (include) => this.constructor.queryBuilder.include(include, this.user);
32
- _getAccessFilter = () => this.constructor.getAccessFilter(this.user);
33
- _hasAccess = (data) => this.constructor.hasAccess(data, this.user) || false;
34
- _omit = () => this.constructor.queryBuilder.omit(this.user);
32
+ _select = (fields) => this.queryBuilder.select(fields);
33
+ _filter = (q) => this.queryBuilder.filter(q);
34
+ _include = (include) => this.queryBuilder.include(include, this.user);
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);
41
+ _omit = () => this.queryBuilder.omit(this.user);
35
42
 
36
43
  /**
37
44
  *
@@ -50,8 +57,7 @@ class Model {
50
57
  sortBy = sortBy.trim();
51
58
  sortOrder = sortOrder.trim();
52
59
  if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
53
- const message = getTranslation("invalid_sort_field", {sortBy, modelName: this.constructor.name});
54
- throw new ErrorResponse(message, 400);
60
+ throw new ErrorResponse(400, "invalid_sort_field", {sortBy, modelName: this.constructor.name});
55
61
  }
56
62
 
57
63
  // Query the database using Prisma with filters, pagination, and limits
@@ -114,8 +120,13 @@ class Model {
114
120
  * @returns {Promise<Object>}
115
121
  */
116
122
  _create = async (data, options = {}) => {
123
+ // CHECK CREATE PERMISSION
124
+ if (!this.canCreate()) {
125
+ throw new ErrorResponse(403, "no_permission_to_create");
126
+ }
127
+
117
128
  // VALIDATE PASSED FIELDS AND RELATIONSHIPS
118
- this.constructor.queryBuilder.create(data, this.user_id);
129
+ this.queryBuilder.create(data, this.user_id);
119
130
 
120
131
  // CREATE
121
132
  return await this.prisma.create({
@@ -132,19 +143,28 @@ class Model {
132
143
  */
133
144
  _update = async (id, data, options = {}) => {
134
145
  id = Number(id);
135
- // GET DATA FIRST
136
- const current_data = await this._get(id, "ALL");
146
+
147
+ // CHECK UPDATE PERMISSION
148
+ const updateFilter = this.getUpdateFilter();
149
+ if (updateFilter === false) {
150
+ throw new ErrorResponse(403, "no_permission_to_update");
151
+ }
137
152
 
138
153
  // VALIDATE PASSED FIELDS AND RELATIONSHIPS
139
- this.constructor.queryBuilder.update(id, data, this.user_id);
140
- return await this.prisma.update({
154
+ this.queryBuilder.update(id, data, this.user_id);
155
+ const response = await this.prisma.update({
141
156
  'where': {
142
- 'id': id
157
+ 'id': id,
158
+ ...updateFilter
143
159
  },
144
160
  'data': data,
145
161
  'include': this.include('ALL'),
146
162
  ...options
147
163
  });
164
+ if(response){
165
+ return response;
166
+ }
167
+ throw new ErrorResponse(403, "no_permission");
148
168
  }
149
169
 
150
170
  /**
@@ -163,16 +183,26 @@ class Model {
163
183
  * @returns {Promise<Object>}
164
184
  */
165
185
  _delete = async (id, options = {}) => {
166
- // GET DATA FIRST
167
- const current_data = await this._get(id);
186
+ id = Number(id);
187
+
188
+ // CHECK DELETE PERMISSION
189
+ const deleteFilter = this.getDeleteFilter();
190
+ if (deleteFilter === false) {
191
+ throw new ErrorResponse(403, "no_permission_to_delete");
192
+ }
168
193
 
169
- return await this.prisma.delete({
194
+ const response = await this.prisma.delete({
170
195
  'where': {
171
- id: parseInt(id)
196
+ id: parseInt(id),
197
+ ...deleteFilter
172
198
  },
173
199
  'select': this.select(),
174
200
  ...options
175
201
  });
202
+ if(response){
203
+ return response;
204
+ }
205
+ throw new ErrorResponse(403, "no_permission");
176
206
  }
177
207
 
178
208
  /**
@@ -233,10 +263,10 @@ class Model {
233
263
  return this._include(include);
234
264
  }
235
265
  sort(sortBy, sortOrder) {
236
- return this.constructor.queryBuilder.sort(sortBy, sortOrder);
266
+ return this.queryBuilder.sort(sortBy, sortOrder);
237
267
  }
238
268
  take(limit){
239
- return this.constructor.queryBuilder.take(Number(limit));
269
+ return this.queryBuilder.take(Number(limit));
240
270
  }
241
271
  skip(offset){
242
272
  const parsed = parseInt(offset);
@@ -252,7 +282,7 @@ class Model {
252
282
  */
253
283
  getAccessFilter(){
254
284
  const filter = this._getAccessFilter()
255
- if(this.user.role == "application" || filter == true){
285
+ if(this.user.role == "application" || filter === true){
256
286
  return {};
257
287
  }
258
288
  return this._getAccessFilter();
@@ -267,6 +297,39 @@ class Model {
267
297
  return this.user.role == "application" ? true : this._hasAccess(data, this.user);
268
298
  }
269
299
 
300
+ /**
301
+ * Check if user can create records
302
+ * @returns {boolean}
303
+ */
304
+ canCreate() {
305
+ if(this.user.role == "application") return true;
306
+ return this._canCreate();
307
+ }
308
+
309
+ /**
310
+ * Get update filter for RLS
311
+ * @returns {Object|false}
312
+ */
313
+ getUpdateFilter(){
314
+ const filter = this._getUpdateFilter();
315
+ if(this.user.role == "application" || filter === true){
316
+ return {};
317
+ }
318
+ return filter;
319
+ }
320
+
321
+ /**
322
+ * Get delete filter for RLS
323
+ * @returns {Object|false}
324
+ */
325
+ getDeleteFilter(){
326
+ const filter = this._getDeleteFilter();
327
+ if(this.user.role == "application" || filter === true){
328
+ return {};
329
+ }
330
+ return filter;
331
+ }
332
+
270
333
  set modelName (name){
271
334
  this.name = name;
272
335
  this.prisma = prisma[name];
@@ -294,12 +357,96 @@ module.exports = {Model, QueryBuilder, prisma};
294
357
  * Generate rapidd/rapidd.js file
295
358
  */
296
359
  function generateRapiddFile(rapiddJsPath) {
297
- const content = `const { PrismaClient, Prisma } = require('../prisma/client');
360
+ const content = `const { PrismaClient } = require('../prisma/client');
361
+ const { AsyncLocalStorage } = require('async_hooks');
298
362
  const rls = require('./rls');
299
363
 
300
- 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
+ };
301
373
 
302
- module.exports = {prisma, Prisma, rls};
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
+ }
401
+
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
+ };
303
450
  `;
304
451
 
305
452
  // Ensure rapidd directory exists
@@ -638,7 +785,11 @@ async function buildModels(options) {
638
785
  // Parse datasource from Prisma schema to get database URL
639
786
  const datasource = parseDatasource(schemaPath);
640
787
 
641
- 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) {
642
793
  // Update only specific model in rls.js
643
794
  await updateRLSForModel(filteredModels, models, rlsPath, datasource, options.userTable, relationships, options.debug);
644
795
  } else {
@@ -655,10 +806,9 @@ async function buildModels(options) {
655
806
  }
656
807
  } catch (error) {
657
808
  console.error('Failed to generate RLS:', error.message);
658
- if (!options.model) {
659
- console.log('Generating permissive RLS fallback...');
660
- await generateRLS(models, rlsPath, null, false, options.userTable, relationships, options.debug);
661
- }
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);
662
812
  }
663
813
  }
664
814
 
@@ -12,21 +12,10 @@ function generateModelFile(modelName, modelInfo) {
12
12
  const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
13
13
 
14
14
  return `const {Model, QueryBuilder, prisma} = require('../Model');
15
- const {rls} = require('../../rapidd/rapidd');
16
15
 
17
16
  class ${className} extends Model {
18
17
  constructor(options){
19
- super('${modelName}', options);
20
- }
21
-
22
- static queryBuilder = new QueryBuilder('${modelName}', rls.model.${modelName} || {});
23
-
24
- static getAccessFilter(user) {
25
- return rls.model.${modelName}?.getAccessFilter?.(user) || {};
26
- }
27
-
28
- static hasAccess(data, user) {
29
- return rls.model.${modelName}?.hasAccess?.(data, user) || true;
18
+ super('${className}', options);
30
19
  }
31
20
 
32
21
  /**
@@ -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