@rapidd/build 1.1.2 β 1.2.0
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/README.md
CHANGED
|
@@ -13,6 +13,11 @@ Dynamic code generator that transforms Prisma schemas into complete Express.js C
|
|
|
13
13
|
- πΊοΈ **Relationships JSON** - Generates complete relationship mappings with foreign keys
|
|
14
14
|
- β‘ **Selective Generation** - Update only specific models or components
|
|
15
15
|
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- **Prisma 7+** (recommended) - Full support for Prisma 7's new architecture
|
|
19
|
+
- Node.js 14.0.0 or higher
|
|
20
|
+
|
|
16
21
|
## Installation
|
|
17
22
|
|
|
18
23
|
```bash
|
|
@@ -155,6 +160,27 @@ npx rapidd build --model user --only model
|
|
|
155
160
|
npx rapidd build --model user --only acl
|
|
156
161
|
```
|
|
157
162
|
|
|
163
|
+
## Migration from Prisma 6 to 7
|
|
164
|
+
|
|
165
|
+
If you're upgrading from Prisma 6, this package now automatically:
|
|
166
|
+
|
|
167
|
+
1. **Uses `@prisma/internals`** for DMMF access (no longer relies on generated client)
|
|
168
|
+
2. **Reads database URL** from multiple sources in order:
|
|
169
|
+
- `prisma.config.ts` (Prisma 7 default)
|
|
170
|
+
- Schema file `datasource.url` (Prisma 5/6 style)
|
|
171
|
+
- `DATABASE_URL` environment variable
|
|
172
|
+
3. **Maintains full compatibility** - no changes needed to your workflow
|
|
173
|
+
|
|
174
|
+
Simply update your dependencies and rebuild:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm install @rapidd/build@latest
|
|
178
|
+
npm install prisma@^7.0.0 @prisma/client@^7.0.0
|
|
179
|
+
npx rapidd build
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
For more details on Prisma 7 migration, see the [official Prisma upgrade guide](https://www.prisma.io/docs/orm/more/upgrade-guides/upgrading-versions/upgrading-to-prisma-7).
|
|
183
|
+
|
|
158
184
|
## License
|
|
159
185
|
|
|
160
186
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidd/build",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|
|
@@ -49,15 +49,18 @@
|
|
|
49
49
|
"README.md"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@prisma/
|
|
52
|
+
"@prisma/adapter-planetscale": "^7.0.1",
|
|
53
|
+
"@prisma/client": "^7.0.0",
|
|
54
|
+
"@prisma/internals": "^7.0.0",
|
|
53
55
|
"commander": "^11.0.0",
|
|
54
56
|
"dotenv": "^16.0.0",
|
|
55
|
-
"pg": "^8.16.3"
|
|
57
|
+
"pg": "^8.16.3",
|
|
58
|
+
"undici": "^7.16.0"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
|
-
"prisma": "^
|
|
61
|
+
"prisma": "^7.0.0"
|
|
59
62
|
},
|
|
60
63
|
"peerDependencies": {
|
|
61
|
-
"@prisma/client": "^
|
|
64
|
+
"@prisma/client": "^7.0.0"
|
|
62
65
|
}
|
|
63
66
|
}
|
package/src/commands/build.js
CHANGED
|
@@ -29,9 +29,20 @@ 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
|
+
|
|
37
|
+
get fields(){
|
|
38
|
+
return this.queryBuilder.fields;
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
_select = (fields) => this.queryBuilder.select(fields);
|
|
33
42
|
_filter = (q) => this.queryBuilder.filter(q);
|
|
34
43
|
_include = (include) => this.queryBuilder.include(include, this.user);
|
|
44
|
+
_queryCreate = (data) => this.queryBuilder.create(data, this.user);
|
|
45
|
+
_queryUpdate = (id, data) => this.queryBuilder.update(id, data, this.user);
|
|
35
46
|
// ACL METHODS
|
|
36
47
|
_canCreate = () => this.acl.canCreate(this.user);
|
|
37
48
|
_getAccessFilter = () => this.acl.getAccessFilter?.(this.user);
|
|
@@ -47,18 +58,19 @@ class Model {
|
|
|
47
58
|
* @param {number} offset
|
|
48
59
|
* @param {string} sortBy
|
|
49
60
|
* @param {'asc'|'desc'} sortOrder
|
|
61
|
+
* @param {{}} [options={}]
|
|
50
62
|
* @returns {Promise<Object[]>}
|
|
51
63
|
*/
|
|
52
|
-
_getMany = async (q = {}, include = "", limit = 25, offset = 0, sortBy =
|
|
64
|
+
_getMany = async (q = {}, include = "", limit = 25, offset = 0, sortBy = this.primaryKey, sortOrder = "asc", options = {})=>{
|
|
53
65
|
const take = this.take(Number(limit));
|
|
54
66
|
const skip = this.skip(Number(offset));
|
|
55
67
|
|
|
56
|
-
sortBy = sortBy
|
|
57
|
-
sortOrder = sortOrder
|
|
68
|
+
sortBy = sortBy?.trim();
|
|
69
|
+
sortOrder = sortOrder?.trim();
|
|
58
70
|
if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
|
|
59
71
|
throw new ErrorResponse(400, "invalid_sort_field", {sortBy, modelName: this.constructor.name});
|
|
60
72
|
}
|
|
61
|
-
|
|
73
|
+
|
|
62
74
|
// Query the database using Prisma with filters, pagination, and limits
|
|
63
75
|
const [data, total] = await prismaTransaction([
|
|
64
76
|
(tx) => tx[this.name].findMany({
|
|
@@ -79,15 +91,16 @@ class Model {
|
|
|
79
91
|
/**
|
|
80
92
|
* @param {number} id
|
|
81
93
|
* @param {string | Object} include
|
|
94
|
+
* @param {{}} [options={}]
|
|
82
95
|
* @returns {Promise<{} | null>}
|
|
83
96
|
*/
|
|
84
97
|
_get = async (id, include, options = {}) =>{
|
|
85
98
|
const {omit, ..._options} = options;
|
|
86
|
-
|
|
99
|
+
console.log(JSON.stringify(this.include(include)));
|
|
87
100
|
// To determine if the record is inaccessible, either due to non-existence or insufficient permissions, two simultaneous queries are performed.
|
|
88
101
|
const _response = this.prisma.findUnique({
|
|
89
102
|
'where': {
|
|
90
|
-
|
|
103
|
+
[this.primaryKey]: id,
|
|
91
104
|
},
|
|
92
105
|
'include': this.include(include),
|
|
93
106
|
'omit': {...this._omit(), ...omit},
|
|
@@ -96,7 +109,7 @@ class Model {
|
|
|
96
109
|
|
|
97
110
|
const _checkPermission = this.prisma.findUnique({
|
|
98
111
|
'where': {
|
|
99
|
-
|
|
112
|
+
[this.primaryKey]: id,
|
|
100
113
|
...this.getAccessFilter()
|
|
101
114
|
},
|
|
102
115
|
'select': {
|
|
@@ -107,21 +120,22 @@ class Model {
|
|
|
107
120
|
const [response, checkPermission] = await Promise.all([_response, _checkPermission]);
|
|
108
121
|
if(response){
|
|
109
122
|
if(checkPermission){
|
|
110
|
-
if(response.id !=
|
|
111
|
-
throw new ErrorResponse(
|
|
123
|
+
if(response.id != checkPermission?.id){ // IN CASE access_filter CONTAINS id FIELD
|
|
124
|
+
throw new ErrorResponse(403, "no_permission");
|
|
112
125
|
}
|
|
113
126
|
}
|
|
114
127
|
else{
|
|
115
|
-
throw new ErrorResponse(
|
|
128
|
+
throw new ErrorResponse(403, "no_permission");
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
else{
|
|
119
|
-
throw new ErrorResponse(
|
|
132
|
+
throw new ErrorResponse(404, "record_not_found");
|
|
120
133
|
}
|
|
121
134
|
return response;
|
|
122
135
|
}
|
|
123
136
|
/**
|
|
124
137
|
* @param {{}} data
|
|
138
|
+
* @param {{}} [options={}]
|
|
125
139
|
* @returns {Promise<Object>}
|
|
126
140
|
*/
|
|
127
141
|
_create = async (data, options = {}) => {
|
|
@@ -131,7 +145,7 @@ class Model {
|
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
// VALIDATE PASSED FIELDS AND RELATIONSHIPS
|
|
134
|
-
this.
|
|
148
|
+
this._queryCreate(data);
|
|
135
149
|
|
|
136
150
|
// CREATE
|
|
137
151
|
return await this.prisma.create({
|
|
@@ -144,11 +158,12 @@ class Model {
|
|
|
144
158
|
/**
|
|
145
159
|
* @param {number} id
|
|
146
160
|
* @param {{}} data
|
|
161
|
+
* @param {{}} [options={}]
|
|
147
162
|
* @returns {Promise<Object>}
|
|
148
163
|
*/
|
|
149
164
|
_update = async (id, data, options = {}) => {
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
delete data.createdAt;
|
|
166
|
+
delete data.createdBy;
|
|
152
167
|
// CHECK UPDATE PERMISSION
|
|
153
168
|
const updateFilter = this.getUpdateFilter();
|
|
154
169
|
if (updateFilter === false) {
|
|
@@ -156,10 +171,10 @@ class Model {
|
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
// VALIDATE PASSED FIELDS AND RELATIONSHIPS
|
|
159
|
-
this.
|
|
174
|
+
this._queryUpdate(id, data);
|
|
160
175
|
const response = await this.prisma.update({
|
|
161
176
|
'where': {
|
|
162
|
-
|
|
177
|
+
[this.primaryKey]: id,
|
|
163
178
|
...updateFilter
|
|
164
179
|
},
|
|
165
180
|
'data': data,
|
|
@@ -172,6 +187,28 @@ class Model {
|
|
|
172
187
|
throw new ErrorResponse(403, "no_permission");
|
|
173
188
|
}
|
|
174
189
|
|
|
190
|
+
/**
|
|
191
|
+
* @param {{}} data
|
|
192
|
+
* @param {string} [unique_key=this.primaryKey]
|
|
193
|
+
* @param {{}} [options={}]
|
|
194
|
+
* @returns {Promise<Object>}
|
|
195
|
+
*/
|
|
196
|
+
async _upsert(data, unique_key = this.primaryKey, options = {}){
|
|
197
|
+
const createData = data;
|
|
198
|
+
const updateData = JSON.parse(JSON.stringify(data));
|
|
199
|
+
this.queryBuilder.create(createData, this.user);
|
|
200
|
+
this.queryBuilder.update(updateData, this.user);
|
|
201
|
+
return await this.prisma.upsert({
|
|
202
|
+
'where': {
|
|
203
|
+
[unique_key]: data[unique_key]
|
|
204
|
+
},
|
|
205
|
+
'create': createData,
|
|
206
|
+
'update': updateData,
|
|
207
|
+
'include': this.include('ALL'),
|
|
208
|
+
...options
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
/**
|
|
176
213
|
*
|
|
177
214
|
* @param {string} q
|
|
@@ -185,11 +222,10 @@ class Model {
|
|
|
185
222
|
|
|
186
223
|
/**
|
|
187
224
|
* @param {number} id
|
|
225
|
+
* @param {{}} [options={}]
|
|
188
226
|
* @returns {Promise<Object>}
|
|
189
227
|
*/
|
|
190
228
|
_delete = async (id, options = {}) => {
|
|
191
|
-
id = Number(id);
|
|
192
|
-
|
|
193
229
|
// CHECK DELETE PERMISSION
|
|
194
230
|
const deleteFilter = this.getDeleteFilter();
|
|
195
231
|
if (deleteFilter === false) {
|
|
@@ -198,7 +234,7 @@ class Model {
|
|
|
198
234
|
|
|
199
235
|
const response = await this.prisma.delete({
|
|
200
236
|
'where': {
|
|
201
|
-
|
|
237
|
+
[this.primaryKey]: id,
|
|
202
238
|
...deleteFilter
|
|
203
239
|
},
|
|
204
240
|
'select': this.select(),
|
|
@@ -226,19 +262,31 @@ class Model {
|
|
|
226
262
|
/**
|
|
227
263
|
* @param {number} id
|
|
228
264
|
* @param {string | Object} include
|
|
265
|
+
* @param {{}} [options={}]
|
|
229
266
|
* @returns {Promise<{} | null>}
|
|
230
267
|
*/
|
|
231
268
|
async get(id, include, options = {}){
|
|
232
|
-
return await this._get(
|
|
269
|
+
return await this._get(id, include, options);
|
|
233
270
|
}
|
|
234
271
|
|
|
235
272
|
/**
|
|
236
273
|
* @param {number} id
|
|
237
274
|
* @param {{}} data
|
|
275
|
+
* @param {{}} [options={}]
|
|
238
276
|
* @returns {Promise<Object>}
|
|
239
277
|
*/
|
|
240
278
|
async update(id, data, options = {}){
|
|
241
|
-
return await this._update(
|
|
279
|
+
return await this._update(id, data, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @param {{}} data
|
|
284
|
+
* @param {string} [unique_key=this.primaryKey]
|
|
285
|
+
* @param {{}} [options={}]
|
|
286
|
+
* @returns {Promise<Object>}
|
|
287
|
+
*/
|
|
288
|
+
async upsert(data, unique_key = this.primaryKey, options = {}){
|
|
289
|
+
return await this._upsert(data, unique_key, options);
|
|
242
290
|
}
|
|
243
291
|
|
|
244
292
|
/**
|
|
@@ -255,7 +303,7 @@ class Model {
|
|
|
255
303
|
* @returns {Promise<Object>}
|
|
256
304
|
*/
|
|
257
305
|
async delete(id, data, options = {}){
|
|
258
|
-
return await this._delete(
|
|
306
|
+
return await this._delete(id, data, options);
|
|
259
307
|
}
|
|
260
308
|
|
|
261
309
|
select(fields){
|
|
@@ -329,15 +377,13 @@ class Model {
|
|
|
329
377
|
set modelName (name){
|
|
330
378
|
this.name = name;
|
|
331
379
|
this.prisma = prisma[name];
|
|
332
|
-
this.fields = this.prisma.fields;
|
|
333
380
|
}
|
|
334
381
|
|
|
335
382
|
static relatedObjects = [];
|
|
336
383
|
static Error = ErrorResponse;
|
|
337
384
|
}
|
|
338
385
|
|
|
339
|
-
module.exports = {Model, QueryBuilder, prisma}
|
|
340
|
-
`;
|
|
386
|
+
module.exports = {Model, QueryBuilder, prisma};`;
|
|
341
387
|
|
|
342
388
|
// Ensure src directory exists
|
|
343
389
|
const srcDir = path.dirname(modelJsPath);
|
|
@@ -359,7 +405,7 @@ function generateRapiddFile(rapiddJsPath, isPostgreSQL = true) {
|
|
|
359
405
|
|
|
360
406
|
if (isPostgreSQL) {
|
|
361
407
|
// PostgreSQL version with RLS support
|
|
362
|
-
content = `const { PrismaClient } = require('../prisma/client');
|
|
408
|
+
content = `const { PrismaClient, Prisma } = require('../prisma/client');
|
|
363
409
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
364
410
|
const acl = require('./acl');
|
|
365
411
|
|
|
@@ -373,13 +419,49 @@ const RLS_CONFIG = {
|
|
|
373
419
|
userRole: process.env.RLS_USER_ROLE || 'current_user_role',
|
|
374
420
|
};
|
|
375
421
|
|
|
376
|
-
//
|
|
422
|
+
// =====================================================
|
|
423
|
+
// BASE PRISMA CLIENTS
|
|
424
|
+
// =====================================================
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* ADMIN CLIENT - Bypasses ALL RLS
|
|
428
|
+
* Uses DATABASE_URL_ADMIN connection (e.g., app_auth_proxy user)
|
|
429
|
+
* Use ONLY for authentication operations:
|
|
430
|
+
* - Login
|
|
431
|
+
* - Register
|
|
432
|
+
* - Email Verification
|
|
433
|
+
* - Password Reset
|
|
434
|
+
* - OAuth operations
|
|
435
|
+
*/
|
|
436
|
+
const authPrisma = new PrismaClient({
|
|
437
|
+
datasources: {
|
|
438
|
+
db: {
|
|
439
|
+
url: process.env.DATABASE_URL_ADMIN
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* BASE CLIENT - Regular user with RLS
|
|
447
|
+
* Uses DATABASE_URL connection
|
|
448
|
+
* Use for all business operations
|
|
449
|
+
*/
|
|
377
450
|
const basePrisma = new PrismaClient({
|
|
451
|
+
datasources: {
|
|
452
|
+
db: {
|
|
453
|
+
url: process.env.DATABASE_URL
|
|
454
|
+
}
|
|
455
|
+
},
|
|
378
456
|
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
379
457
|
});
|
|
380
458
|
|
|
459
|
+
// =====================================================
|
|
460
|
+
// RLS HELPER FUNCTIONS
|
|
461
|
+
// =====================================================
|
|
462
|
+
|
|
381
463
|
/**
|
|
382
|
-
*
|
|
464
|
+
* Set RLS Session Variables in PostgreSQL
|
|
383
465
|
* Execute each SET command separately to avoid prepared statement error
|
|
384
466
|
*/
|
|
385
467
|
async function setRLSVariables(tx, userId, userRole) {
|
|
@@ -387,27 +469,48 @@ async function setRLSVariables(tx, userId, userRole) {
|
|
|
387
469
|
const userIdVar = RLS_CONFIG.userId;
|
|
388
470
|
const userRoleVar = RLS_CONFIG.userRole;
|
|
389
471
|
|
|
390
|
-
// Execute SET commands separately
|
|
391
|
-
await tx.$executeRawUnsafe(\`SET LOCAL
|
|
392
|
-
await tx.$executeRawUnsafe(\`SET LOCAL
|
|
472
|
+
// Execute SET commands separately
|
|
473
|
+
await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userIdVar} = '\${userId}'\`);
|
|
474
|
+
await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userRoleVar} = '\${userRole}'\`);
|
|
393
475
|
}
|
|
394
476
|
|
|
395
|
-
|
|
477
|
+
/**
|
|
478
|
+
* Reset RLS Session Variables
|
|
479
|
+
*/
|
|
480
|
+
async function resetRLSVariables(tx) {
|
|
481
|
+
const namespace = RLS_CONFIG.namespace;
|
|
482
|
+
const userIdVar = RLS_CONFIG.userId;
|
|
483
|
+
const userRoleVar = RLS_CONFIG.userRole;
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
await tx.$executeRawUnsafe(\`RESET \${namespace}.\${userIdVar}\`);
|
|
487
|
+
await tx.$executeRawUnsafe(\`RESET \${namespace}.\${userRoleVar}\`);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
// Ignore errors on reset
|
|
490
|
+
console.error('Failed to reset RLS variables:', e);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// =====================================================
|
|
495
|
+
// EXTENDED PRISMA WITH AUTOMATIC RLS
|
|
496
|
+
// =====================================================
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Extended Prisma Client with automatic RLS context
|
|
500
|
+
* Automatically wraps all operations in RLS context from AsyncLocalStorage
|
|
501
|
+
*/
|
|
396
502
|
const prisma = basePrisma.$extends({
|
|
397
503
|
query: {
|
|
398
504
|
async $allOperations({ operation, args, query, model }) {
|
|
399
505
|
const context = requestContext.getStore();
|
|
400
506
|
|
|
401
|
-
//
|
|
507
|
+
// No context = no RLS (e.g., system operations)
|
|
402
508
|
if (!context?.userId || !context?.userRole) {
|
|
403
509
|
return query(args);
|
|
404
510
|
}
|
|
405
511
|
|
|
406
512
|
const { userId, userRole } = context;
|
|
407
513
|
|
|
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
514
|
// For operations that are already transactions, just set the variables
|
|
412
515
|
if (operation === '$transaction') {
|
|
413
516
|
return basePrisma.$transaction(async (tx) => {
|
|
@@ -422,7 +525,6 @@ const prisma = basePrisma.$extends({
|
|
|
422
525
|
await setRLSVariables(tx, userId, userRole);
|
|
423
526
|
|
|
424
527
|
// Execute the original query using the transaction client
|
|
425
|
-
// This is the key: we need to use the transaction client for the query
|
|
426
528
|
if (model) {
|
|
427
529
|
// Model query (e.g., user.findMany())
|
|
428
530
|
return tx[model][operation](args);
|
|
@@ -435,70 +537,31 @@ const prisma = basePrisma.$extends({
|
|
|
435
537
|
},
|
|
436
538
|
});
|
|
437
539
|
|
|
438
|
-
//
|
|
540
|
+
// =====================================================
|
|
541
|
+
// TRANSACTION HELPERS
|
|
542
|
+
// =====================================================
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Helper for batch operations in single transaction
|
|
546
|
+
*/
|
|
439
547
|
async function prismaTransaction(operations) {
|
|
440
548
|
const context = requestContext.getStore();
|
|
441
|
-
|
|
442
|
-
if (!context?.userId || !context?.userRole) {
|
|
443
|
-
return Promise.all(operations);
|
|
444
|
-
}
|
|
445
|
-
|
|
549
|
+
|
|
446
550
|
return basePrisma.$transaction(async (tx) => {
|
|
447
|
-
|
|
551
|
+
if (context?.userId && context?.userRole) {
|
|
552
|
+
await setRLSVariables(tx, context.userId, context.userRole);
|
|
553
|
+
}
|
|
448
554
|
return Promise.all(operations.map(op => op(tx)));
|
|
449
555
|
});
|
|
450
556
|
}
|
|
451
557
|
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
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();
|
|
558
|
+
// =====================================================
|
|
559
|
+
// CONTEXT HELPERS
|
|
560
|
+
// =====================================================
|
|
499
561
|
|
|
500
562
|
/**
|
|
501
563
|
* Express Middleware: Set RLS context from authenticated user
|
|
564
|
+
* Use this AFTER your authentication middleware
|
|
502
565
|
*/
|
|
503
566
|
function setRLSContext(req, res, next) {
|
|
504
567
|
if (req.user) {
|
|
@@ -516,74 +579,50 @@ function setRLSContext(req, res, next) {
|
|
|
516
579
|
}
|
|
517
580
|
|
|
518
581
|
/**
|
|
519
|
-
*
|
|
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)
|
|
582
|
+
* Get RLS Config (for SQL generation)
|
|
550
583
|
*/
|
|
551
584
|
function getRLSConfig() {
|
|
552
585
|
return RLS_CONFIG;
|
|
553
586
|
}
|
|
554
587
|
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
// Option 1: Using extended prisma (automatic RLS)
|
|
559
|
-
const users = await prisma.user.findMany();
|
|
560
|
-
|
|
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
|
-
});
|
|
588
|
+
// =====================================================
|
|
589
|
+
// GRACEFUL SHUTDOWN
|
|
590
|
+
// =====================================================
|
|
565
591
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
592
|
+
async function disconnectAll() {
|
|
593
|
+
await authPrisma.$disconnect();
|
|
594
|
+
await basePrisma.$disconnect();
|
|
595
|
+
}
|
|
569
596
|
|
|
570
|
-
|
|
597
|
+
process.on('beforeExit', async () => {
|
|
598
|
+
await disconnectAll();
|
|
571
599
|
});
|
|
572
|
-
|
|
600
|
+
|
|
601
|
+
// =====================================================
|
|
602
|
+
// EXPORTS
|
|
603
|
+
// =====================================================
|
|
573
604
|
|
|
574
605
|
module.exports = {
|
|
575
|
-
|
|
606
|
+
// Main clients
|
|
607
|
+
prisma, // Use for regular operations with automatic RLS from context
|
|
608
|
+
authPrisma, // Use ONLY for auth operations (login, register, etc.)
|
|
609
|
+
|
|
610
|
+
// Transaction helpers
|
|
576
611
|
prismaTransaction,
|
|
577
|
-
|
|
578
|
-
|
|
612
|
+
|
|
613
|
+
// Context helpers
|
|
579
614
|
requestContext,
|
|
580
615
|
setRLSContext,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
transactionWithRLS,
|
|
584
|
-
prismaWithRLS,
|
|
585
|
-
getRLSConfig,
|
|
616
|
+
|
|
617
|
+
// RLS utilities
|
|
586
618
|
setRLSVariables,
|
|
619
|
+
resetRLSVariables,
|
|
620
|
+
getRLSConfig,
|
|
621
|
+
|
|
622
|
+
// Utilities
|
|
623
|
+
disconnectAll,
|
|
624
|
+
PrismaClient,
|
|
625
|
+
Prisma,
|
|
587
626
|
acl
|
|
588
627
|
};
|
|
589
628
|
`;
|
|
@@ -623,7 +662,7 @@ module.exports = {
|
|
|
623
662
|
/**
|
|
624
663
|
* Update relationships.json for a specific model
|
|
625
664
|
*/
|
|
626
|
-
async function updateRelationshipsForModel(filteredModels, relationshipsPath,
|
|
665
|
+
async function updateRelationshipsForModel(filteredModels, relationshipsPath, schemaPath, usedDMMF) {
|
|
627
666
|
let existingRelationships = {};
|
|
628
667
|
|
|
629
668
|
// Load existing relationships if file exists
|
|
@@ -641,7 +680,7 @@ async function updateRelationshipsForModel(filteredModels, relationshipsPath, pr
|
|
|
641
680
|
// Use DMMF to get relationships for specific model
|
|
642
681
|
const { generateRelationshipsFromDMMF } = require('../generators/relationshipsGenerator');
|
|
643
682
|
const tempPath = relationshipsPath + '.tmp';
|
|
644
|
-
await generateRelationshipsFromDMMF(
|
|
683
|
+
await generateRelationshipsFromDMMF(schemaPath, tempPath);
|
|
645
684
|
const allRelationships = JSON.parse(fs.readFileSync(tempPath, 'utf8'));
|
|
646
685
|
fs.unlinkSync(tempPath);
|
|
647
686
|
|
|
@@ -835,15 +874,14 @@ async function buildModels(options) {
|
|
|
835
874
|
console.warn('Continuing with schema parsing fallback...\n');
|
|
836
875
|
}
|
|
837
876
|
|
|
838
|
-
// Try to use Prisma DMMF first (
|
|
877
|
+
// Try to use Prisma DMMF first (using @prisma/internals getDMMF)
|
|
839
878
|
let parsedData = null;
|
|
840
|
-
const prismaClientPath = path.join(process.cwd(), 'prisma', 'client');
|
|
841
879
|
let usedDMMF = false;
|
|
842
880
|
|
|
843
881
|
try {
|
|
844
|
-
parsedData = await parsePrismaDMMF(
|
|
882
|
+
parsedData = await parsePrismaDMMF(schemaPath);
|
|
845
883
|
if (parsedData) {
|
|
846
|
-
console.log('Using Prisma
|
|
884
|
+
console.log('Using Prisma DMMF (via @prisma/internals)');
|
|
847
885
|
usedDMMF = true;
|
|
848
886
|
}
|
|
849
887
|
} catch (error) {
|
|
@@ -899,11 +937,14 @@ async function buildModels(options) {
|
|
|
899
937
|
}
|
|
900
938
|
|
|
901
939
|
// Parse datasource to determine database type
|
|
902
|
-
let datasource = { isPostgreSQL: true }; // Default to PostgreSQL
|
|
940
|
+
let datasource = { isPostgreSQL: true, url: null }; // Default to PostgreSQL
|
|
903
941
|
try {
|
|
904
942
|
datasource = parseDatasource(schemaPath);
|
|
905
943
|
} catch (error) {
|
|
906
|
-
|
|
944
|
+
// Only warn if it's not the expected "No url found" error in Prisma 7
|
|
945
|
+
if (!error.message.includes('No url found')) {
|
|
946
|
+
console.warn('Could not parse datasource, assuming PostgreSQL:', error.message);
|
|
947
|
+
}
|
|
907
948
|
}
|
|
908
949
|
|
|
909
950
|
// Generate rapidd/rapidd.js if it doesn't exist
|
|
@@ -919,11 +960,11 @@ async function buildModels(options) {
|
|
|
919
960
|
try {
|
|
920
961
|
if (options.model) {
|
|
921
962
|
// Update only specific model in relationships.json
|
|
922
|
-
await updateRelationshipsForModel(filteredModels, relationshipsPath,
|
|
963
|
+
await updateRelationshipsForModel(filteredModels, relationshipsPath, schemaPath, usedDMMF);
|
|
923
964
|
} else {
|
|
924
965
|
// Generate all relationships
|
|
925
966
|
if (usedDMMF) {
|
|
926
|
-
await generateRelationshipsFromDMMF(
|
|
967
|
+
await generateRelationshipsFromDMMF(schemaPath, relationshipsPath);
|
|
927
968
|
} else {
|
|
928
969
|
generateRelationshipsFromSchema(schemaPath, relationshipsPath);
|
|
929
970
|
}
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
65
|
+
return await this._delete(id);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
@@ -184,12 +184,12 @@ function generateRelationshipsFromSchema(schemaPath, outputPath) {
|
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Generate relationships.json from DMMF
|
|
187
|
-
* @param {string}
|
|
187
|
+
* @param {string} schemaPath - Path to Prisma schema file
|
|
188
188
|
* @param {string} outputPath - Path to output relationships.json
|
|
189
189
|
*/
|
|
190
|
-
async function generateRelationshipsFromDMMF(
|
|
190
|
+
async function generateRelationshipsFromDMMF(schemaPath, outputPath) {
|
|
191
191
|
const { parsePrismaDMMF } = require('../parsers/prismaParser');
|
|
192
|
-
const parsedData = await parsePrismaDMMF(
|
|
192
|
+
const parsedData = await parsePrismaDMMF(schemaPath);
|
|
193
193
|
generateRelationships(parsedData.models, outputPath);
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -8,6 +8,39 @@ try {
|
|
|
8
8
|
// dotenv not available, skip
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Try to load DATABASE_URL from prisma.config.ts (Prisma 7)
|
|
13
|
+
* @returns {string|null} - Database URL or null if not found
|
|
14
|
+
*/
|
|
15
|
+
function loadUrlFromPrismaConfig() {
|
|
16
|
+
const configPath = path.join(process.cwd(), 'prisma.config.ts');
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(configPath)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
24
|
+
|
|
25
|
+
// Look for env('DATABASE_URL') or similar patterns
|
|
26
|
+
const envMatch = configContent.match(/env\(['"]([^'"]+)['"]\)/);
|
|
27
|
+
if (envMatch) {
|
|
28
|
+
const envVar = envMatch[1];
|
|
29
|
+
return process.env[envVar] || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Look for direct URL assignment
|
|
33
|
+
const urlMatch = configContent.match(/url:\s*['"]([^'"]+)['"]/);
|
|
34
|
+
if (urlMatch) {
|
|
35
|
+
return urlMatch[1];
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Failed to read config, return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
11
44
|
/**
|
|
12
45
|
* Parse datasource configuration from Prisma schema
|
|
13
46
|
* @param {string} schemaPath - Path to Prisma schema file
|
|
@@ -30,26 +63,32 @@ function parseDatasource(schemaPath) {
|
|
|
30
63
|
const providerMatch = datasourceBlock.match(/provider\s*=\s*"([^"]+)"/);
|
|
31
64
|
const provider = providerMatch ? providerMatch[1] : null;
|
|
32
65
|
|
|
33
|
-
//
|
|
66
|
+
// Try to extract url from schema first
|
|
67
|
+
let url = null;
|
|
34
68
|
const urlMatch = datasourceBlock.match(/url\s*=\s*(.+)/);
|
|
35
|
-
if (!urlMatch) {
|
|
36
|
-
throw new Error('No url found in datasource block');
|
|
37
|
-
}
|
|
38
69
|
|
|
39
|
-
|
|
70
|
+
if (urlMatch) {
|
|
71
|
+
url = urlMatch[1].trim();
|
|
72
|
+
|
|
73
|
+
// Handle env() function
|
|
74
|
+
const envMatch = url.match(/env\(["']([^"']+)["']\)/);
|
|
75
|
+
if (envMatch) {
|
|
76
|
+
const envVar = envMatch[1];
|
|
77
|
+
url = process.env[envVar];
|
|
78
|
+
} else {
|
|
79
|
+
// Remove quotes if present
|
|
80
|
+
url = url.replace(/^["']|["']$/g, '');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
40
83
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
url = process.env[envVar];
|
|
84
|
+
// If no URL in schema, try prisma.config.ts (Prisma 7)
|
|
85
|
+
if (!url) {
|
|
86
|
+
url = loadUrlFromPrismaConfig();
|
|
87
|
+
}
|
|
46
88
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
} else {
|
|
51
|
-
// Remove quotes if present
|
|
52
|
-
url = url.replace(/^["']|["']$/g, '');
|
|
89
|
+
// If still no URL, check DATABASE_URL environment variable directly
|
|
90
|
+
if (!url) {
|
|
91
|
+
url = process.env.DATABASE_URL || null;
|
|
53
92
|
}
|
|
54
93
|
|
|
55
94
|
// Detect PostgreSQL from provider OR from the actual connection URL
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Extract blocks (model or enum) with proper brace matching
|
|
@@ -204,16 +203,26 @@ function parseEnumValues(enumBody) {
|
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
/**
|
|
207
|
-
* Use Prisma's
|
|
206
|
+
* Use Prisma's DMMF (Data Model Meta Format) to get model information
|
|
208
207
|
* This is an alternative approach that uses Prisma's own abstraction
|
|
209
|
-
* @
|
|
208
|
+
* In Prisma 7, we use getDMMF from @prisma/internals instead of accessing it from the client
|
|
209
|
+
* @param {string} schemaPath - Path to Prisma schema file
|
|
210
210
|
* @returns {Object} - Models extracted from DMMF
|
|
211
211
|
*/
|
|
212
|
-
async function parsePrismaDMMF(
|
|
212
|
+
async function parsePrismaDMMF(schemaPath) {
|
|
213
213
|
try {
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
const
|
|
214
|
+
// In Prisma 7, DMMF is no longer exposed on the client
|
|
215
|
+
// We need to use getDMMF from @prisma/internals instead
|
|
216
|
+
const { getDMMF } = require('@prisma/internals');
|
|
217
|
+
const fs = require('fs');
|
|
218
|
+
|
|
219
|
+
// Read the schema file
|
|
220
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
221
|
+
|
|
222
|
+
// Get DMMF from the schema
|
|
223
|
+
const dmmf = await getDMMF({
|
|
224
|
+
datamodel: schemaContent
|
|
225
|
+
});
|
|
217
226
|
|
|
218
227
|
const models = {};
|
|
219
228
|
|
|
@@ -266,7 +275,8 @@ async function parsePrismaDMMF(prismaClientPath) {
|
|
|
266
275
|
|
|
267
276
|
return { models, enums: dmmf.datamodel.enums };
|
|
268
277
|
} catch (error) {
|
|
269
|
-
console.warn('Could not
|
|
278
|
+
console.warn('Could not use getDMMF from @prisma/internals, falling back to schema parsing');
|
|
279
|
+
console.warn('Error:', error.message);
|
|
270
280
|
return null;
|
|
271
281
|
}
|
|
272
282
|
}
|