@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 +1 -1
- package/src/commands/build.js +180 -30
- package/src/generators/modelGenerator.js +1 -12
- package/src/parsers/datasourceParser.js +10 -1
package/package.json
CHANGED
package/src/commands/build.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
30
|
-
_filter = (q) => this.
|
|
31
|
-
_include = (include) => this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
136
|
-
|
|
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.
|
|
140
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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.
|
|
266
|
+
return this.queryBuilder.sort(sortBy, sortOrder);
|
|
237
267
|
}
|
|
238
268
|
take(limit){
|
|
239
|
-
return this.
|
|
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
|
|
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
|
|
360
|
+
const content = `const { PrismaClient } = require('../prisma/client');
|
|
361
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
298
362
|
const rls = require('./rls');
|
|
299
363
|
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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('${
|
|
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
|
|