@rapidd/build 1.0.0 β 1.0.2
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 +90 -7
- package/bin/cli.js +3 -0
- package/package.json +4 -4
- package/src/commands/build.js +289 -62
- package/src/generators/rlsGenerator.js +8 -6
- package/src/generators/rlsGeneratorV2.js +11 -7
- package/src/generators/routeGenerator.js +22 -28
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Dynamic code generator that transforms Prisma schemas into complete Express.js C
|
|
|
11
11
|
- π₯ **Role-Based Access Control** - Properly handles role checks in filters
|
|
12
12
|
- π **Model Generation** - Creates CRUD model classes with capitalized filenames
|
|
13
13
|
- πΊοΈ **Relationships JSON** - Generates complete relationship mappings with foreign keys
|
|
14
|
+
- β‘ **Selective Generation** - Update only specific models or components
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -21,16 +22,77 @@ npm install @rapidd/build
|
|
|
21
22
|
## Quick Start
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
|
-
# Generate in current directory (default)
|
|
25
|
+
# Generate everything in current directory (default)
|
|
25
26
|
npx rapidd build
|
|
26
27
|
|
|
27
28
|
# Generate in specific directory
|
|
28
29
|
npx rapidd build --output ./generated
|
|
29
30
|
|
|
31
|
+
# Generate only specific model
|
|
32
|
+
npx rapidd build --model user
|
|
33
|
+
|
|
34
|
+
# Generate only specific component
|
|
35
|
+
npx rapidd build --only model
|
|
36
|
+
npx rapidd build --only route
|
|
37
|
+
npx rapidd build --only rls
|
|
38
|
+
npx rapidd build --only relationship
|
|
39
|
+
|
|
40
|
+
# Combine model and component filters
|
|
41
|
+
npx rapidd build --model account --only route
|
|
42
|
+
|
|
30
43
|
# Specify custom user table
|
|
31
44
|
npx rapidd build --user-table accounts
|
|
32
45
|
```
|
|
33
46
|
|
|
47
|
+
## CLI Options
|
|
48
|
+
|
|
49
|
+
- `-o, --output <path>` - Output directory (default: `./`)
|
|
50
|
+
- `-s, --schema <path>` - Prisma schema file (default: `./prisma/schema.prisma`)
|
|
51
|
+
- `-m, --model <name>` - Generate/update only specific model (e.g., "account", "user")
|
|
52
|
+
- `--only <component>` - Generate only specific component: "model", "route", "rls", or "relationship"
|
|
53
|
+
- `--user-table <name>` - User table name for RLS (default: auto-detected)
|
|
54
|
+
|
|
55
|
+
## Selective Generation
|
|
56
|
+
|
|
57
|
+
### Update Single Model
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Update only the account model across all components
|
|
61
|
+
npx rapidd build --model account
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This will:
|
|
65
|
+
- Generate/update `src/Model/Account.js`
|
|
66
|
+
- Generate/update `routes/api/v1/account.js`
|
|
67
|
+
- Update the `account` entry in `rapidd/relationships.json`
|
|
68
|
+
- Update the `account` entry in `rapidd/rls.js`
|
|
69
|
+
|
|
70
|
+
### Update Single Component
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Regenerate all routes
|
|
74
|
+
npx rapidd build --only route
|
|
75
|
+
|
|
76
|
+
# Regenerate all RLS configs
|
|
77
|
+
npx rapidd build --only rls
|
|
78
|
+
|
|
79
|
+
# Regenerate all models
|
|
80
|
+
npx rapidd build --only model
|
|
81
|
+
|
|
82
|
+
# Regenerate relationships
|
|
83
|
+
npx rapidd build --only relationship
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Combine Filters
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Update only the route for a specific model
|
|
90
|
+
npx rapidd build --model user --only route
|
|
91
|
+
|
|
92
|
+
# Update RLS for account model
|
|
93
|
+
npx rapidd build --model account --only rls
|
|
94
|
+
```
|
|
95
|
+
|
|
34
96
|
## Generated Structure
|
|
35
97
|
|
|
36
98
|
```
|
|
@@ -69,18 +131,39 @@ getAccessFilter: (user) => {
|
|
|
69
131
|
}
|
|
70
132
|
```
|
|
71
133
|
|
|
72
|
-
## CLI Options
|
|
73
|
-
|
|
74
|
-
- `-o, --output <path>` - Output directory (default: `./`)
|
|
75
|
-
- `-s, --schema <path>` - Prisma schema file (default: `./prisma/schema.prisma`)
|
|
76
|
-
- `-u, --user-table <name>` - User table name for RLS (default: auto-detected)
|
|
77
|
-
|
|
78
134
|
## Usage with PostgreSQL RLS
|
|
79
135
|
|
|
80
136
|
```bash
|
|
81
137
|
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" npx rapidd build
|
|
82
138
|
```
|
|
83
139
|
|
|
140
|
+
## Use Cases
|
|
141
|
+
|
|
142
|
+
### During Development
|
|
143
|
+
```bash
|
|
144
|
+
# After adding a new model to schema
|
|
145
|
+
npx rapidd build --model newModel
|
|
146
|
+
|
|
147
|
+
# After changing relationships
|
|
148
|
+
npx rapidd build --only relationship
|
|
149
|
+
|
|
150
|
+
# After updating RLS policies
|
|
151
|
+
npx rapidd build --only rls
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Continuous Integration
|
|
155
|
+
```bash
|
|
156
|
+
# Full rebuild for CI/CD
|
|
157
|
+
npx rapidd build --output ./generated
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Incremental Updates
|
|
161
|
+
```bash
|
|
162
|
+
# Update specific model after schema changes
|
|
163
|
+
npx rapidd build --model user --only model
|
|
164
|
+
npx rapidd build --model user --only rls
|
|
165
|
+
```
|
|
166
|
+
|
|
84
167
|
## License
|
|
85
168
|
|
|
86
169
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -16,7 +16,10 @@ program
|
|
|
16
16
|
.description('Build model files from Prisma schema')
|
|
17
17
|
.option('-s, --schema <path>', 'Path to Prisma schema file', process.env.PRISMA_SCHEMA_PATH || './prisma/schema.prisma')
|
|
18
18
|
.option('-o, --output <path>', 'Output base directory', './')
|
|
19
|
+
.option('-m, --model <name>', 'Generate/update only specific model (e.g., "account", "user")')
|
|
20
|
+
.option('--only <component>', 'Generate only specific component: "model", "route", "rls", or "relationship"')
|
|
19
21
|
.option('--user-table <name>', 'Name of the user table for RLS (default: auto-detect from user/users)')
|
|
22
|
+
.option('--debug', 'Enable debug mode (generates rls-mappings.json)')
|
|
20
23
|
.action(async (options) => {
|
|
21
24
|
try {
|
|
22
25
|
await buildModels(options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidd/build",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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,12 +12,12 @@
|
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/rapidd
|
|
15
|
+
"url": "https://github.com/MertDalbudak/rapidd-build"
|
|
16
16
|
},
|
|
17
17
|
"bugs": {
|
|
18
|
-
"url": "https://github.com/rapidd
|
|
18
|
+
"url": "https://github.com/MertDalbudak/rapidd-build/issues"
|
|
19
19
|
},
|
|
20
|
-
"homepage": "https://github.com/rapidd
|
|
20
|
+
"homepage": "https://github.com/MertDalbudak/rapidd-build#readme",
|
|
21
21
|
"keywords": [
|
|
22
22
|
"rapidd",
|
|
23
23
|
"prisma",
|
package/src/commands/build.js
CHANGED
|
@@ -12,7 +12,7 @@ const { generateAllRoutes } = require('../generators/routeGenerator');
|
|
|
12
12
|
*/
|
|
13
13
|
function generateBaseModelFile(modelJsPath) {
|
|
14
14
|
const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
|
|
15
|
-
const {ErrorResponse} = require('./Api');
|
|
15
|
+
const {ErrorResponse, getTranslation} = require('./Api');
|
|
16
16
|
|
|
17
17
|
class Model {
|
|
18
18
|
/**
|
|
@@ -50,7 +50,8 @@ class Model {
|
|
|
50
50
|
sortBy = sortBy.trim();
|
|
51
51
|
sortOrder = sortOrder.trim();
|
|
52
52
|
if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
|
|
53
|
-
|
|
53
|
+
const message = getTranslation("invalid_sort_field", {sortBy, modelName: this.constructor.name});
|
|
54
|
+
throw new ErrorResponse(message, 400);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// Query the database using Prisma with filters, pagination, and limits
|
|
@@ -71,37 +72,40 @@ class Model {
|
|
|
71
72
|
*/
|
|
72
73
|
_get = async (id, include, options = {}) =>{
|
|
73
74
|
const {omit, ..._options} = options;
|
|
74
|
-
id = Number(id)
|
|
75
|
+
id = Number(id);
|
|
75
76
|
// To determine if the record is inaccessible, either due to non-existence or insufficient permissions, two simultaneous queries are performed.
|
|
76
77
|
const _response = this.prisma.findUnique({
|
|
77
78
|
'where': {
|
|
78
|
-
'id': id
|
|
79
|
-
...this.getAccessFilter()
|
|
79
|
+
'id': id
|
|
80
80
|
},
|
|
81
81
|
'include': this.include(include),
|
|
82
82
|
'omit': {...this._omit(), ...omit},
|
|
83
83
|
..._options
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
const
|
|
86
|
+
const _checkPermission = this.prisma.findUnique({
|
|
87
87
|
'where': {
|
|
88
|
-
'id': id
|
|
88
|
+
'id': id,
|
|
89
|
+
...this.getAccessFilter()
|
|
89
90
|
},
|
|
90
91
|
'select': {
|
|
91
92
|
'id': true
|
|
92
93
|
}
|
|
93
94
|
});
|
|
94
95
|
|
|
95
|
-
const [response,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const [response, checkPermission] = await Promise.all([_response, _checkPermission]);
|
|
97
|
+
if(response){
|
|
98
|
+
if(checkPermission){
|
|
99
|
+
if(response.id != checkExistence?.id){ // IN CASE access_filter CONTAINS id FIELD
|
|
100
|
+
throw new ErrorResponse(getTranslation("no_permission"), 403);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else{
|
|
104
|
+
throw new ErrorResponse(getTranslation("no_permission"), 403);
|
|
100
105
|
}
|
|
101
|
-
throw new ErrorResponse("No permission", 403);
|
|
102
106
|
}
|
|
103
|
-
|
|
104
|
-
throw new ErrorResponse("
|
|
107
|
+
else{
|
|
108
|
+
throw new ErrorResponse(getTranslation("record_not_found"), 404);
|
|
105
109
|
}
|
|
106
110
|
return response;
|
|
107
111
|
}
|
|
@@ -276,6 +280,12 @@ class Model {
|
|
|
276
280
|
module.exports = {Model, QueryBuilder, prisma};
|
|
277
281
|
`;
|
|
278
282
|
|
|
283
|
+
// Ensure src directory exists
|
|
284
|
+
const srcDir = path.dirname(modelJsPath);
|
|
285
|
+
if (!fs.existsSync(srcDir)) {
|
|
286
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
|
|
279
289
|
fs.writeFileSync(modelJsPath, content);
|
|
280
290
|
console.log('β Generated src/Model.js');
|
|
281
291
|
}
|
|
@@ -302,12 +312,175 @@ module.exports = {prisma, Prisma, rls};
|
|
|
302
312
|
console.log('β Generated rapidd/rapidd.js');
|
|
303
313
|
}
|
|
304
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Update relationships.json for a specific model
|
|
317
|
+
*/
|
|
318
|
+
async function updateRelationshipsForModel(filteredModels, relationshipsPath, prismaClientPath, schemaPath, usedDMMF) {
|
|
319
|
+
let existingRelationships = {};
|
|
320
|
+
|
|
321
|
+
// Load existing relationships if file exists
|
|
322
|
+
if (fs.existsSync(relationshipsPath)) {
|
|
323
|
+
try {
|
|
324
|
+
existingRelationships = JSON.parse(fs.readFileSync(relationshipsPath, 'utf8'));
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.warn('Could not parse existing relationships.json, will create new');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Generate relationships for the filtered model(s)
|
|
331
|
+
let newRelationships = {};
|
|
332
|
+
if (usedDMMF) {
|
|
333
|
+
// Use DMMF to get relationships for specific model
|
|
334
|
+
const { generateRelationshipsFromDMMF } = require('../generators/relationshipsGenerator');
|
|
335
|
+
const tempPath = relationshipsPath + '.tmp';
|
|
336
|
+
await generateRelationshipsFromDMMF(prismaClientPath, tempPath);
|
|
337
|
+
const allRelationships = JSON.parse(fs.readFileSync(tempPath, 'utf8'));
|
|
338
|
+
fs.unlinkSync(tempPath);
|
|
339
|
+
|
|
340
|
+
// Extract only the filtered model's relationships
|
|
341
|
+
for (const modelName of Object.keys(filteredModels)) {
|
|
342
|
+
if (allRelationships[modelName]) {
|
|
343
|
+
newRelationships[modelName] = allRelationships[modelName];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
// Use schema parser
|
|
348
|
+
const { generateRelationshipsFromSchema } = require('../generators/relationshipsGenerator');
|
|
349
|
+
const tempPath = relationshipsPath + '.tmp';
|
|
350
|
+
generateRelationshipsFromSchema(schemaPath, tempPath);
|
|
351
|
+
const allRelationships = JSON.parse(fs.readFileSync(tempPath, 'utf8'));
|
|
352
|
+
fs.unlinkSync(tempPath);
|
|
353
|
+
|
|
354
|
+
// Extract only the filtered model's relationships
|
|
355
|
+
for (const modelName of Object.keys(filteredModels)) {
|
|
356
|
+
if (allRelationships[modelName]) {
|
|
357
|
+
newRelationships[modelName] = allRelationships[modelName];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Merge with existing relationships
|
|
363
|
+
const updatedRelationships = { ...existingRelationships, ...newRelationships };
|
|
364
|
+
|
|
365
|
+
// Write back to file
|
|
366
|
+
fs.writeFileSync(relationshipsPath, JSON.stringify(updatedRelationships, null, 2));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Update rls.js for a specific model
|
|
371
|
+
*/
|
|
372
|
+
async function updateRLSForModel(filteredModels, allModels, rlsPath, datasource, userTable, relationships, debug = false) {
|
|
373
|
+
const { generateRLS } = require('../generators/rlsGeneratorV2');
|
|
374
|
+
|
|
375
|
+
// Generate RLS for the filtered model (but pass all models for user table detection)
|
|
376
|
+
const tempPath = rlsPath + '.tmp';
|
|
377
|
+
await generateRLS(
|
|
378
|
+
filteredModels,
|
|
379
|
+
tempPath,
|
|
380
|
+
datasource.url,
|
|
381
|
+
datasource.isPostgreSQL,
|
|
382
|
+
userTable,
|
|
383
|
+
relationships,
|
|
384
|
+
debug,
|
|
385
|
+
allModels
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Read the generated RLS for the specific model
|
|
389
|
+
const tempContent = fs.readFileSync(tempPath, 'utf8');
|
|
390
|
+
fs.unlinkSync(tempPath);
|
|
391
|
+
|
|
392
|
+
// Extract the model's RLS configuration
|
|
393
|
+
const modelName = Object.keys(filteredModels)[0];
|
|
394
|
+
|
|
395
|
+
// Find the start of the model definition
|
|
396
|
+
const modelStart = tempContent.indexOf(`${modelName}:`);
|
|
397
|
+
if (modelStart === -1) {
|
|
398
|
+
throw new Error(`Could not find model ${modelName} in generated RLS`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Find the matching closing brace by counting braces
|
|
402
|
+
let braceCount = 0;
|
|
403
|
+
let inString = false;
|
|
404
|
+
let stringChar = null;
|
|
405
|
+
let i = tempContent.indexOf('{', modelStart);
|
|
406
|
+
const contentStart = i;
|
|
407
|
+
|
|
408
|
+
for (; i < tempContent.length; i++) {
|
|
409
|
+
const char = tempContent[i];
|
|
410
|
+
const prevChar = i > 0 ? tempContent[i - 1] : '';
|
|
411
|
+
|
|
412
|
+
// Handle string literals
|
|
413
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
414
|
+
if (!inString) {
|
|
415
|
+
inString = true;
|
|
416
|
+
stringChar = char;
|
|
417
|
+
} else if (char === stringChar) {
|
|
418
|
+
inString = false;
|
|
419
|
+
stringChar = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!inString) {
|
|
424
|
+
if (char === '{') braceCount++;
|
|
425
|
+
if (char === '}') braceCount--;
|
|
426
|
+
|
|
427
|
+
if (braceCount === 0) {
|
|
428
|
+
// Found the closing brace
|
|
429
|
+
const modelRls = tempContent.substring(modelStart, i + 1);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (braceCount !== 0) {
|
|
436
|
+
throw new Error(`Could not extract RLS for model ${modelName} - unmatched braces`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const modelRls = tempContent.substring(modelStart, i + 1);
|
|
440
|
+
|
|
441
|
+
// Read existing rls.js
|
|
442
|
+
if (fs.existsSync(rlsPath)) {
|
|
443
|
+
let existingContent = fs.readFileSync(rlsPath, 'utf8');
|
|
444
|
+
|
|
445
|
+
// Check if model already exists in RLS
|
|
446
|
+
const existingModelPattern = new RegExp(`${modelName}:\\s*\\{[\\s\\S]*?\\n \\}(?=,|\\n)`);
|
|
447
|
+
|
|
448
|
+
if (existingModelPattern.test(existingContent)) {
|
|
449
|
+
// Replace existing model RLS
|
|
450
|
+
existingContent = existingContent.replace(existingModelPattern, modelRls);
|
|
451
|
+
} else {
|
|
452
|
+
// Add new model RLS before the closing of rls.model
|
|
453
|
+
// Find the last closing brace of a model object and add comma after it
|
|
454
|
+
existingContent = existingContent.replace(
|
|
455
|
+
/(\n \})\n(\};)/,
|
|
456
|
+
`$1,\n ${modelRls}\n$2`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
fs.writeFileSync(rlsPath, existingContent);
|
|
461
|
+
console.log(`β Updated RLS for model: ${modelName}`);
|
|
462
|
+
} else {
|
|
463
|
+
// If rls.js doesn't exist, create it with just this model
|
|
464
|
+
await generateRLS(
|
|
465
|
+
filteredModels,
|
|
466
|
+
rlsPath,
|
|
467
|
+
datasource.url,
|
|
468
|
+
datasource.isPostgreSQL,
|
|
469
|
+
userTable,
|
|
470
|
+
relationships,
|
|
471
|
+
debug,
|
|
472
|
+
allModels
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
305
477
|
/**
|
|
306
478
|
* Build models from Prisma schema
|
|
307
479
|
* @param {Object} options - Build options
|
|
308
480
|
* @param {string} options.schema - Path to Prisma schema file
|
|
309
481
|
* @param {string} options.output - Output directory for generated models
|
|
310
|
-
* @param {string} options.
|
|
482
|
+
* @param {string} options.model - Optional: specific model to generate
|
|
483
|
+
* @param {string} options.only - Optional: specific component to generate
|
|
311
484
|
*/
|
|
312
485
|
async function buildModels(options) {
|
|
313
486
|
const schemaPath = path.resolve(process.cwd(), options.schema);
|
|
@@ -378,67 +551,121 @@ async function buildModels(options) {
|
|
|
378
551
|
|
|
379
552
|
const { models, enums } = parsedData;
|
|
380
553
|
|
|
381
|
-
|
|
554
|
+
// Filter models if --model option is provided
|
|
555
|
+
let filteredModels = models;
|
|
556
|
+
if (options.model) {
|
|
557
|
+
const modelName = options.model.toLowerCase();
|
|
558
|
+
const matchedModel = Object.keys(models).find(m => m.toLowerCase() === modelName);
|
|
382
559
|
|
|
383
|
-
|
|
384
|
-
|
|
560
|
+
if (!matchedModel) {
|
|
561
|
+
throw new Error(`Model "${options.model}" not found in schema. Available models: ${Object.keys(models).join(', ')}`);
|
|
562
|
+
}
|
|
385
563
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
564
|
+
filteredModels = { [matchedModel]: models[matchedModel] };
|
|
565
|
+
console.log(`Filtering to model: ${matchedModel}`);
|
|
566
|
+
}
|
|
389
567
|
|
|
390
|
-
|
|
391
|
-
console.log('Generating rapidd/rapidd.js...');
|
|
392
|
-
generateRapiddFile(rapiddJsPath);
|
|
568
|
+
console.log(`Found ${Object.keys(models).length} models${options.model ? ` (generating ${Object.keys(filteredModels).length})` : ''}`);
|
|
393
569
|
|
|
394
|
-
//
|
|
395
|
-
|
|
570
|
+
// Determine which components to generate
|
|
571
|
+
const shouldGenerate = {
|
|
572
|
+
model: !options.only || options.only === 'model',
|
|
573
|
+
route: !options.only || options.only === 'route',
|
|
574
|
+
rls: !options.only || options.only === 'rls',
|
|
575
|
+
relationship: !options.only || options.only === 'relationship'
|
|
576
|
+
};
|
|
396
577
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
} else {
|
|
401
|
-
generateRelationshipsFromSchema(schemaPath, relationshipsPath);
|
|
402
|
-
}
|
|
403
|
-
console.log(`β Relationships file generated at: ${relationshipsPath}`);
|
|
404
|
-
} catch (error) {
|
|
405
|
-
console.error('Failed to generate relationships.json:', error.message);
|
|
406
|
-
console.log('Note: You may need to create relationships.json manually.');
|
|
578
|
+
// Validate --only option
|
|
579
|
+
if (options.only && !['model', 'route', 'rls', 'relationship'].includes(options.only)) {
|
|
580
|
+
throw new Error(`Invalid --only value "${options.only}". Must be one of: model, route, rls, relationship`);
|
|
407
581
|
}
|
|
408
582
|
|
|
409
|
-
// Generate
|
|
410
|
-
|
|
583
|
+
// Generate model files
|
|
584
|
+
if (shouldGenerate.model) {
|
|
585
|
+
generateAllModels(filteredModels, modelDir, modelJsPath);
|
|
586
|
+
}
|
|
411
587
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
588
|
+
// Generate src/Model.js (base Model class) if it doesn't exist
|
|
589
|
+
if (!fs.existsSync(modelJsPath)) {
|
|
590
|
+
console.log('\nGenerating src/Model.js...');
|
|
591
|
+
generateBaseModelFile(modelJsPath);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Generate rapidd/rapidd.js if it doesn't exist
|
|
595
|
+
if (!fs.existsSync(rapiddJsPath)) {
|
|
596
|
+
console.log('Generating rapidd/rapidd.js...');
|
|
597
|
+
generateRapiddFile(rapiddJsPath);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Generate relationships.json
|
|
601
|
+
if (shouldGenerate.relationship) {
|
|
602
|
+
console.log(`\nGenerating relationships.json...`);
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
if (options.model) {
|
|
606
|
+
// Update only specific model in relationships.json
|
|
607
|
+
await updateRelationshipsForModel(filteredModels, relationshipsPath, prismaClientPath, schemaPath, usedDMMF);
|
|
608
|
+
} else {
|
|
609
|
+
// Generate all relationships
|
|
610
|
+
if (usedDMMF) {
|
|
611
|
+
await generateRelationshipsFromDMMF(prismaClientPath, relationshipsPath);
|
|
612
|
+
} else {
|
|
613
|
+
generateRelationshipsFromSchema(schemaPath, relationshipsPath);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
console.log(`β Relationships file generated at: ${relationshipsPath}`);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error('Failed to generate relationships.json:', error.message);
|
|
619
|
+
console.log('Note: You may need to create relationships.json manually.');
|
|
417
620
|
}
|
|
418
|
-
} catch (error) {
|
|
419
|
-
console.warn('Could not load relationships.json:', error.message);
|
|
420
621
|
}
|
|
421
622
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
623
|
+
// Generate RLS configuration
|
|
624
|
+
if (shouldGenerate.rls) {
|
|
625
|
+
console.log(`\nGenerating RLS configuration...`);
|
|
626
|
+
|
|
627
|
+
// Load relationships for Prisma filter building
|
|
628
|
+
let relationships = {};
|
|
629
|
+
try {
|
|
630
|
+
if (fs.existsSync(relationshipsPath)) {
|
|
631
|
+
relationships = JSON.parse(fs.readFileSync(relationshipsPath, 'utf8'));
|
|
632
|
+
}
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.warn('Could not load relationships.json:', error.message);
|
|
635
|
+
}
|
|
425
636
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
637
|
+
try {
|
|
638
|
+
// Parse datasource from Prisma schema to get database URL
|
|
639
|
+
const datasource = parseDatasource(schemaPath);
|
|
640
|
+
|
|
641
|
+
if (options.model) {
|
|
642
|
+
// Update only specific model in rls.js
|
|
643
|
+
await updateRLSForModel(filteredModels, models, rlsPath, datasource, options.userTable, relationships, options.debug);
|
|
644
|
+
} else {
|
|
645
|
+
// Generate RLS for all models
|
|
646
|
+
await generateRLS(
|
|
647
|
+
models,
|
|
648
|
+
rlsPath,
|
|
649
|
+
datasource.url,
|
|
650
|
+
datasource.isPostgreSQL,
|
|
651
|
+
options.userTable,
|
|
652
|
+
relationships,
|
|
653
|
+
options.debug
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
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
|
+
}
|
|
662
|
+
}
|
|
438
663
|
}
|
|
439
664
|
|
|
440
665
|
// Generate routes
|
|
441
|
-
|
|
666
|
+
if (shouldGenerate.route) {
|
|
667
|
+
generateAllRoutes(filteredModels, routesDir);
|
|
668
|
+
}
|
|
442
669
|
|
|
443
670
|
return { models, enums };
|
|
444
671
|
}
|
|
@@ -259,7 +259,7 @@ function generateFilter(policies, expressionField) {
|
|
|
259
259
|
* @param {boolean} isPostgreSQL - Whether database is PostgreSQL
|
|
260
260
|
* @param {string} userTableOption - User-specified table name
|
|
261
261
|
*/
|
|
262
|
-
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption) {
|
|
262
|
+
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, debug = false) {
|
|
263
263
|
const userTable = detectUserTable(models, userTableOption);
|
|
264
264
|
const modelNames = Object.keys(models);
|
|
265
265
|
|
|
@@ -277,11 +277,13 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
|
|
|
277
277
|
functionAnalysis = await analyzeFunctions(databaseUrl);
|
|
278
278
|
console.log(`β Analyzed ${Object.keys(functionAnalysis.functionMappings).length} PostgreSQL functions`);
|
|
279
279
|
|
|
280
|
-
// Save function analysis for debugging/manual adjustment
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
// Save function analysis for debugging/manual adjustment (only if --debug flag is set)
|
|
281
|
+
if (debug) {
|
|
282
|
+
const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
|
|
283
|
+
const mappingConfig = generateMappingConfig(functionAnalysis);
|
|
284
|
+
fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
|
|
285
|
+
console.log(`β Function mappings saved to ${configPath}`);
|
|
286
|
+
}
|
|
285
287
|
} catch (error) {
|
|
286
288
|
console.warn(`β Could not analyze functions: ${error.message}`);
|
|
287
289
|
}
|
|
@@ -285,8 +285,10 @@ function buildConditionalFilter(filtersWithRoles) {
|
|
|
285
285
|
/**
|
|
286
286
|
* Generate complete rls.js file
|
|
287
287
|
*/
|
|
288
|
-
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}) {
|
|
289
|
-
|
|
288
|
+
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}, debug = false, allModels = null) {
|
|
289
|
+
// Use allModels for user table detection if provided (when filtering by model)
|
|
290
|
+
const modelsForUserDetection = allModels || models;
|
|
291
|
+
const userTable = detectUserTable(modelsForUserDetection, userTableOption);
|
|
290
292
|
const modelNames = Object.keys(models);
|
|
291
293
|
|
|
292
294
|
let policies = {};
|
|
@@ -313,11 +315,13 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
|
|
|
313
315
|
relationships
|
|
314
316
|
);
|
|
315
317
|
|
|
316
|
-
// Save function analysis for debugging
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
// Save function analysis for debugging (only if --debug flag is set)
|
|
319
|
+
if (debug) {
|
|
320
|
+
const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
|
|
321
|
+
const mappingConfig = generateMappingConfig(functionAnalysis);
|
|
322
|
+
fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
|
|
323
|
+
console.log(`β Function mappings saved to ${configPath}`);
|
|
324
|
+
}
|
|
321
325
|
|
|
322
326
|
// Also add user context requirements as a comment in rls.js
|
|
323
327
|
if (Object.keys(functionAnalysis.userContextRequirements).length > 0) {
|
|
@@ -7,11 +7,9 @@ const path = require('path');
|
|
|
7
7
|
* @returns {string} - Generated route code
|
|
8
8
|
*/
|
|
9
9
|
function generateRouteFile(modelName) {
|
|
10
|
-
const modelNameLower = modelName.toLowerCase();
|
|
11
10
|
const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
12
11
|
|
|
13
12
|
return `const router = require('express').Router();
|
|
14
|
-
const {Api, ErrorResponse} = require('../../../src/Api');
|
|
15
13
|
const {${className}, QueryBuilder, prisma} = require('../../../src/Model/${className}');
|
|
16
14
|
|
|
17
15
|
router.all('*', async (req, res, next) => {
|
|
@@ -20,13 +18,12 @@ router.all('*', async (req, res, next) => {
|
|
|
20
18
|
next();
|
|
21
19
|
}
|
|
22
20
|
else{
|
|
23
|
-
res.
|
|
21
|
+
return res.sendError(401, req.getTranslation("no_valid_session"));
|
|
24
22
|
}
|
|
25
23
|
});
|
|
26
24
|
|
|
27
25
|
// GET ALL
|
|
28
26
|
router.get('/', async function(req, res) {
|
|
29
|
-
let response, status_code = 200;
|
|
30
27
|
try {
|
|
31
28
|
const { q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc" } = req.query;
|
|
32
29
|
|
|
@@ -34,67 +31,64 @@ router.get('/', async function(req, res) {
|
|
|
34
31
|
const _count = req.${className}.count(q);
|
|
35
32
|
const [data, count] = await Promise.all([_data, _count]);
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
return res.sendList(data, {'take': req.${className}.take(Number(limit)), 'skip': req.${className}.skip(Number(offset)), 'total': count});
|
|
38
35
|
}
|
|
39
36
|
catch(error){
|
|
40
|
-
response = QueryBuilder.errorHandler(error);
|
|
41
|
-
|
|
37
|
+
const response = QueryBuilder.errorHandler(error);
|
|
38
|
+
return res.status(response.status_code).send(response);
|
|
42
39
|
}
|
|
43
|
-
res.status(status_code).send(response);
|
|
44
40
|
});
|
|
45
41
|
|
|
46
42
|
// GET BY ID
|
|
47
43
|
router.get('/:id', async function(req, res) {
|
|
48
|
-
let response, status_code = 200;
|
|
49
44
|
try{
|
|
50
45
|
const { include = ""} = req.query;
|
|
51
|
-
response = await req.${className}.get(req.params.id, include);
|
|
46
|
+
const response = await req.${className}.get(req.params.id, include);
|
|
47
|
+
return res.json(response);
|
|
52
48
|
}
|
|
53
49
|
catch(error){
|
|
54
|
-
response = QueryBuilder.errorHandler(error);
|
|
55
|
-
|
|
50
|
+
const response = QueryBuilder.errorHandler(error);
|
|
51
|
+
return res.status(response.status_code).send(response);
|
|
56
52
|
}
|
|
57
|
-
res.status(status_code).send(response);
|
|
58
53
|
});
|
|
59
54
|
|
|
60
55
|
// CREATE
|
|
61
56
|
router.post('/', async function(req, res) {
|
|
62
|
-
|
|
57
|
+
const payload = req.body;
|
|
63
58
|
try{
|
|
64
|
-
response = await req.${className}.create(payload);
|
|
59
|
+
const response = await req.${className}.create(payload);
|
|
60
|
+
return res.status(201).json(response);
|
|
65
61
|
}
|
|
66
62
|
catch(error){
|
|
67
|
-
response = QueryBuilder.errorHandler(error, payload);
|
|
68
|
-
|
|
63
|
+
const response = QueryBuilder.errorHandler(error, payload);
|
|
64
|
+
return res.status(response.status_code).send(response);
|
|
69
65
|
}
|
|
70
|
-
res.status(status_code).send(response);
|
|
71
66
|
});
|
|
72
67
|
|
|
73
68
|
// UPDATE
|
|
74
69
|
router.patch('/:id', async function(req, res) {
|
|
75
|
-
|
|
70
|
+
const payload = req.body;
|
|
76
71
|
try{
|
|
77
|
-
response = await req.${className}.update(req.params.id, payload);
|
|
72
|
+
const response = await req.${className}.update(req.params.id, payload);
|
|
73
|
+
return res.json(response);
|
|
78
74
|
}
|
|
79
75
|
catch(error){
|
|
80
|
-
response = QueryBuilder.errorHandler(error, payload);
|
|
81
|
-
|
|
76
|
+
const response = QueryBuilder.errorHandler(error, payload);
|
|
77
|
+
return res.status(response.status_code).send(response);
|
|
82
78
|
}
|
|
83
|
-
res.status(status_code).send(response);
|
|
84
79
|
});
|
|
85
80
|
|
|
86
81
|
// DELETE
|
|
87
82
|
router.delete('/:id', async (req, res)=>{
|
|
88
|
-
let response, status_code = 200;
|
|
89
83
|
try{
|
|
90
84
|
await req.${className}.delete(req.params.id);
|
|
91
|
-
|
|
85
|
+
const message = req.getTranslation("object_deleted_successfully", {modelName: "${className}"});
|
|
86
|
+
return res.sendResponse(200, message);
|
|
92
87
|
}
|
|
93
88
|
catch(error){
|
|
94
|
-
response = QueryBuilder.errorHandler(error);
|
|
95
|
-
|
|
89
|
+
const response = QueryBuilder.errorHandler(error);
|
|
90
|
+
return res.status(response.status_code).send(response);
|
|
96
91
|
}
|
|
97
|
-
res.status(status_code).send(response);
|
|
98
92
|
});
|
|
99
93
|
|
|
100
94
|
module.exports = router;
|