@rapidd/build 1.0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/cli.js +30 -0
- package/index.js +11 -0
- package/package.json +63 -0
- package/src/commands/build.js +448 -0
- package/src/generators/modelGenerator.js +168 -0
- package/src/generators/relationshipsGenerator.js +186 -0
- package/src/generators/rlsGenerator.js +334 -0
- package/src/generators/rlsGeneratorV2.js +381 -0
- package/src/generators/routeGenerator.js +127 -0
- package/src/parsers/advancedRLSConverter.js +305 -0
- package/src/parsers/autoRLSConverter.js +322 -0
- package/src/parsers/datasourceParser.js +73 -0
- package/src/parsers/deepSQLAnalyzer.js +540 -0
- package/src/parsers/dynamicRLSConverter.js +353 -0
- package/src/parsers/enhancedRLSConverter.js +181 -0
- package/src/parsers/functionAnalyzer.js +302 -0
- package/src/parsers/postgresRLSConverter.js +192 -0
- package/src/parsers/prismaFilterBuilder.js +422 -0
- package/src/parsers/prismaParser.js +245 -0
- package/src/parsers/sqlToJsConverter.js +611 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rapidd Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @rapidd/build
|
|
2
|
+
|
|
3
|
+
Dynamic code generator that transforms Prisma schemas into complete Express.js CRUD APIs with intelligent PostgreSQL RLS-to-JavaScript translation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- π **Automatic CRUD API Generation** - Creates Express.js routes from Prisma models
|
|
8
|
+
- π **RLS Translation** - Converts PostgreSQL Row-Level Security policies to JavaScript/Prisma filters
|
|
9
|
+
- π― **Dynamic & Schema-Aware** - Zero hardcoding, adapts to any database structure
|
|
10
|
+
- π **Relationship Handling** - Supports 1:1, 1:n, n:m including junction tables
|
|
11
|
+
- π₯ **Role-Based Access Control** - Properly handles role checks in filters
|
|
12
|
+
- π **Model Generation** - Creates CRUD model classes with capitalized filenames
|
|
13
|
+
- πΊοΈ **Relationships JSON** - Generates complete relationship mappings with foreign keys
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @rapidd/build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Generate in current directory (default)
|
|
25
|
+
npx rapidd build
|
|
26
|
+
|
|
27
|
+
# Generate in specific directory
|
|
28
|
+
npx rapidd build --output ./generated
|
|
29
|
+
|
|
30
|
+
# Specify custom user table
|
|
31
|
+
npx rapidd build --user-table accounts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Generated Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
./
|
|
38
|
+
βββ src/Model/
|
|
39
|
+
β βββ User.js
|
|
40
|
+
β βββ Post.js
|
|
41
|
+
β βββ ...
|
|
42
|
+
βββ routes/
|
|
43
|
+
β βββ user.js
|
|
44
|
+
β βββ post.js
|
|
45
|
+
β βββ ...
|
|
46
|
+
βββ rapidd/
|
|
47
|
+
βββ rls.js
|
|
48
|
+
βββ relationships.json
|
|
49
|
+
βββ rapidd.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## RLS Translation Example
|
|
53
|
+
|
|
54
|
+
**PostgreSQL Policy:**
|
|
55
|
+
```sql
|
|
56
|
+
CREATE POLICY user_policy ON posts
|
|
57
|
+
FOR SELECT
|
|
58
|
+
USING (author_id = current_user_id() OR current_user_role() IN ('admin', 'moderator'));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Generated JavaScript:**
|
|
62
|
+
```javascript
|
|
63
|
+
hasAccess: (data, user) => {
|
|
64
|
+
return data?.author_id === user?.id || ['admin', 'moderator'].includes(user?.role);
|
|
65
|
+
},
|
|
66
|
+
getAccessFilter: (user) => {
|
|
67
|
+
if (['admin', 'moderator'].includes(user?.role)) return {};
|
|
68
|
+
return { author_id: user?.id };
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
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
|
+
## Usage with PostgreSQL RLS
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" npx rapidd build
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { buildModels } = require('../src/commands/build');
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('rapidd')
|
|
11
|
+
.description('Rapidd build tool for generating model files from Prisma schema')
|
|
12
|
+
.version('0.1.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('build')
|
|
16
|
+
.description('Build model files from Prisma schema')
|
|
17
|
+
.option('-s, --schema <path>', 'Path to Prisma schema file', process.env.PRISMA_SCHEMA_PATH || './prisma/schema.prisma')
|
|
18
|
+
.option('-o, --output <path>', 'Output base directory', './')
|
|
19
|
+
.option('--user-table <name>', 'Name of the user table for RLS (default: auto-detect from user/users)')
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
try {
|
|
22
|
+
await buildModels(options);
|
|
23
|
+
console.log('\nβ Build completed successfully');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error building models:', error.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program.parse(process.argv);
|
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { buildModels } = require('./src/commands/build');
|
|
2
|
+
const { parsePrismaSchema, parsePrismaDMMF } = require('./src/parsers/prismaParser');
|
|
3
|
+
const { generateModelFile, generateAllModels } = require('./src/generators/modelGenerator');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
buildModels,
|
|
7
|
+
parsePrismaSchema,
|
|
8
|
+
parsePrismaDMMF,
|
|
9
|
+
generateModelFile,
|
|
10
|
+
generateAllModels
|
|
11
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rapidd/build",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dynamic code generator that transforms Prisma schemas into Express.js CRUD APIs with PostgreSQL RLS-to-JavaScript translation",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rapidd": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"prepublishOnly": "echo 'Ready to publish'"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/rapidd/build"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/rapidd/build/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/rapidd/build#readme",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"rapidd",
|
|
23
|
+
"prisma",
|
|
24
|
+
"code-generation",
|
|
25
|
+
"orm",
|
|
26
|
+
"express",
|
|
27
|
+
"routes",
|
|
28
|
+
"rls",
|
|
29
|
+
"row-level-security",
|
|
30
|
+
"crud",
|
|
31
|
+
"api-generator",
|
|
32
|
+
"postgresql",
|
|
33
|
+
"mysql",
|
|
34
|
+
"database",
|
|
35
|
+
"security",
|
|
36
|
+
"access-control",
|
|
37
|
+
"authentication",
|
|
38
|
+
"authorization"
|
|
39
|
+
],
|
|
40
|
+
"author": "Rapidd Team",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=14.0.0"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"bin/",
|
|
47
|
+
"src/",
|
|
48
|
+
"index.js",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@prisma/client": "^5.0.0",
|
|
53
|
+
"commander": "^11.0.0",
|
|
54
|
+
"dotenv": "^16.0.0",
|
|
55
|
+
"pg": "^8.16.3"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"prisma": "^5.0.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@prisma/client": "^5.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { parsePrismaSchema, parsePrismaDMMF } = require('../parsers/prismaParser');
|
|
4
|
+
const { generateAllModels } = require('../generators/modelGenerator');
|
|
5
|
+
const { generateRelationshipsFromDMMF, generateRelationshipsFromSchema } = require('../generators/relationshipsGenerator');
|
|
6
|
+
const { generateRLS } = require('../generators/rlsGeneratorV2');
|
|
7
|
+
const { parseDatasource } = require('../parsers/datasourceParser');
|
|
8
|
+
const { generateAllRoutes } = require('../generators/routeGenerator');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate src/Model.js base class file
|
|
12
|
+
*/
|
|
13
|
+
function generateBaseModelFile(modelJsPath) {
|
|
14
|
+
const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
|
|
15
|
+
const {ErrorResponse} = require('./Api');
|
|
16
|
+
|
|
17
|
+
class Model {
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} name
|
|
20
|
+
* @param {{'user': {}}} options
|
|
21
|
+
*/
|
|
22
|
+
constructor(name, options){
|
|
23
|
+
this.modelName = name;
|
|
24
|
+
this.options = options || {}
|
|
25
|
+
this.user = this.options.user || {'id': 1, 'role': 'application'};
|
|
26
|
+
this.user_id = this.user ? this.user.id : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
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);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
* @param {string} q
|
|
39
|
+
* @property {string|Object} include
|
|
40
|
+
* @param {number} limit
|
|
41
|
+
* @param {number} offset
|
|
42
|
+
* @param {string} sortBy
|
|
43
|
+
* @param {'asc'|'desc'} sortOrder
|
|
44
|
+
* @returns {Promise<Object[]>}
|
|
45
|
+
*/
|
|
46
|
+
_getMany = async (q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc", options = {})=>{
|
|
47
|
+
const take = this.take(Number(limit));
|
|
48
|
+
const skip = this.skip(Number(offset));
|
|
49
|
+
|
|
50
|
+
sortBy = sortBy.trim();
|
|
51
|
+
sortOrder = sortOrder.trim();
|
|
52
|
+
if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
|
|
53
|
+
throw new ErrorResponse(\`Parameter sortBy '\${sortBy}' is not a valid field of \${this.constructor.name}\`, 400);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Query the database using Prisma with filters, pagination, and limits
|
|
57
|
+
return await this.prisma.findMany({
|
|
58
|
+
'where': this.filter(q),
|
|
59
|
+
'include': this.include(include),
|
|
60
|
+
'take': take,
|
|
61
|
+
'skip': skip,
|
|
62
|
+
'orderBy': this.sort(sortBy, sortOrder),
|
|
63
|
+
'omit': this._omit(),
|
|
64
|
+
...options
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* @param {number} id
|
|
69
|
+
* @param {string | Object} include
|
|
70
|
+
* @returns {Promise<{} | null>}
|
|
71
|
+
*/
|
|
72
|
+
_get = async (id, include, options = {}) =>{
|
|
73
|
+
const {omit, ..._options} = options;
|
|
74
|
+
id = Number(id)
|
|
75
|
+
// To determine if the record is inaccessible, either due to non-existence or insufficient permissions, two simultaneous queries are performed.
|
|
76
|
+
const _response = this.prisma.findUnique({
|
|
77
|
+
'where': {
|
|
78
|
+
'id': id,
|
|
79
|
+
...this.getAccessFilter()
|
|
80
|
+
},
|
|
81
|
+
'include': this.include(include),
|
|
82
|
+
'omit': {...this._omit(), ...omit},
|
|
83
|
+
..._options
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const _checkExistence = this.prisma.findUnique({
|
|
87
|
+
'where': {
|
|
88
|
+
'id': id
|
|
89
|
+
},
|
|
90
|
+
'select': {
|
|
91
|
+
'id': true
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const [response, checkExistence] = await Promise.all([_response, _checkExistence]);
|
|
96
|
+
|
|
97
|
+
if(response == null){
|
|
98
|
+
if(checkExistence == null){
|
|
99
|
+
throw new ErrorResponse("Record not found", 404);
|
|
100
|
+
}
|
|
101
|
+
throw new ErrorResponse("No permission", 403);
|
|
102
|
+
}
|
|
103
|
+
if(response.id != checkExistence?.id){ // IN CASE access_filter CONTAINS id FIELD
|
|
104
|
+
throw new ErrorResponse("No permission", 403);
|
|
105
|
+
}
|
|
106
|
+
return response;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* @param {{}} data
|
|
110
|
+
* @returns {Promise<Object>}
|
|
111
|
+
*/
|
|
112
|
+
_create = async (data, options = {}) => {
|
|
113
|
+
// VALIDATE PASSED FIELDS AND RELATIONSHIPS
|
|
114
|
+
this.constructor.queryBuilder.create(data, this.user_id);
|
|
115
|
+
|
|
116
|
+
// CREATE
|
|
117
|
+
return await this.prisma.create({
|
|
118
|
+
'data': data,
|
|
119
|
+
'include': this.include('ALL'),
|
|
120
|
+
...options
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {number} id
|
|
126
|
+
* @param {{}} data
|
|
127
|
+
* @returns {Promise<Object>}
|
|
128
|
+
*/
|
|
129
|
+
_update = async (id, data, options = {}) => {
|
|
130
|
+
id = Number(id);
|
|
131
|
+
// GET DATA FIRST
|
|
132
|
+
const current_data = await this._get(id, "ALL");
|
|
133
|
+
|
|
134
|
+
// VALIDATE PASSED FIELDS AND RELATIONSHIPS
|
|
135
|
+
this.constructor.queryBuilder.update(id, data, this.user_id);
|
|
136
|
+
return await this.prisma.update({
|
|
137
|
+
'where': {
|
|
138
|
+
'id': id
|
|
139
|
+
},
|
|
140
|
+
'data': data,
|
|
141
|
+
'include': this.include('ALL'),
|
|
142
|
+
...options
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
*
|
|
148
|
+
* @param {string} q
|
|
149
|
+
* @returns {Promise<number>}
|
|
150
|
+
*/
|
|
151
|
+
_count = async (q = {}) => {
|
|
152
|
+
return await this.prisma.count({
|
|
153
|
+
'where': this.filter(q)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {number} id
|
|
159
|
+
* @returns {Promise<Object>}
|
|
160
|
+
*/
|
|
161
|
+
_delete = async (id, options = {}) => {
|
|
162
|
+
// GET DATA FIRST
|
|
163
|
+
const current_data = await this._get(id);
|
|
164
|
+
|
|
165
|
+
return await this.prisma.delete({
|
|
166
|
+
'where': {
|
|
167
|
+
id: parseInt(id)
|
|
168
|
+
},
|
|
169
|
+
'select': this.select(),
|
|
170
|
+
...options
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
*
|
|
176
|
+
* @param {string} q
|
|
177
|
+
* @property {string|Object} include
|
|
178
|
+
* @param {number} limit
|
|
179
|
+
* @param {number} offset
|
|
180
|
+
* @param {string} sortBy
|
|
181
|
+
* @param {'asc'|'desc'} sortOrder
|
|
182
|
+
* @returns {Promise<Object[]>}
|
|
183
|
+
*/
|
|
184
|
+
async getMany(q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc"){
|
|
185
|
+
return await this._getMany(q, include, Number(limit), Number(offset), sortBy, sortOrder);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* @param {number} id
|
|
189
|
+
* @param {string | Object} include
|
|
190
|
+
* @returns {Promise<{} | null>}
|
|
191
|
+
*/
|
|
192
|
+
async get(id, include, options = {}){
|
|
193
|
+
return await this._get(Number(id), include, options);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {number} id
|
|
198
|
+
* @param {{}} data
|
|
199
|
+
* @returns {Promise<Object>}
|
|
200
|
+
*/
|
|
201
|
+
async update(id, data, options = {}){
|
|
202
|
+
return await this._update(Number(id), data, options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
*
|
|
207
|
+
* @param {string} q
|
|
208
|
+
* @returns {Promise<number>}
|
|
209
|
+
*/
|
|
210
|
+
async count(q = {}) {
|
|
211
|
+
return await this._count(q);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {number} id
|
|
216
|
+
* @returns {Promise<Object>}
|
|
217
|
+
*/
|
|
218
|
+
async delete(id, data, options = {}){
|
|
219
|
+
return await this._delete(Number(id), data, options);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
select(fields){
|
|
223
|
+
return this._select(fields);
|
|
224
|
+
}
|
|
225
|
+
filter(include){
|
|
226
|
+
return {...this._filter(include), ...this.getAccessFilter()};
|
|
227
|
+
}
|
|
228
|
+
include(include){
|
|
229
|
+
return this._include(include);
|
|
230
|
+
}
|
|
231
|
+
sort(sortBy, sortOrder) {
|
|
232
|
+
return this.constructor.queryBuilder.sort(sortBy, sortOrder);
|
|
233
|
+
}
|
|
234
|
+
take(limit){
|
|
235
|
+
return this.constructor.queryBuilder.take(Number(limit));
|
|
236
|
+
}
|
|
237
|
+
skip(offset){
|
|
238
|
+
const parsed = parseInt(offset);
|
|
239
|
+
if(isNaN(parsed) || parsed < 0){
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
return parsed;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
*
|
|
247
|
+
* @returns {Object}
|
|
248
|
+
*/
|
|
249
|
+
getAccessFilter(){
|
|
250
|
+
const filter = this._getAccessFilter()
|
|
251
|
+
if(this.user.role == "application" || filter == true){
|
|
252
|
+
return {};
|
|
253
|
+
}
|
|
254
|
+
return this._getAccessFilter();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
*
|
|
259
|
+
* @param {*} data
|
|
260
|
+
* @returns {boolean}
|
|
261
|
+
*/
|
|
262
|
+
hasAccess(data) {
|
|
263
|
+
return this.user.role == "application" ? true : this._hasAccess(data, this.user);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
set modelName (name){
|
|
267
|
+
this.name = name;
|
|
268
|
+
this.prisma = prisma[name];
|
|
269
|
+
this.fields = this.prisma.fields;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static relatedObjects = [];
|
|
273
|
+
static Error = ErrorResponse;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = {Model, QueryBuilder, prisma};
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
fs.writeFileSync(modelJsPath, content);
|
|
280
|
+
console.log('β Generated src/Model.js');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate rapidd/rapidd.js file
|
|
285
|
+
*/
|
|
286
|
+
function generateRapiddFile(rapiddJsPath) {
|
|
287
|
+
const content = `const { PrismaClient, Prisma } = require('../prisma/client');
|
|
288
|
+
const rls = require('./rls');
|
|
289
|
+
|
|
290
|
+
const prisma = new PrismaClient();
|
|
291
|
+
|
|
292
|
+
module.exports = {prisma, Prisma, rls};
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
// Ensure rapidd directory exists
|
|
296
|
+
const rapiddDir = path.dirname(rapiddJsPath);
|
|
297
|
+
if (!fs.existsSync(rapiddDir)) {
|
|
298
|
+
fs.mkdirSync(rapiddDir, { recursive: true });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fs.writeFileSync(rapiddJsPath, content);
|
|
302
|
+
console.log('β Generated rapidd/rapidd.js');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Build models from Prisma schema
|
|
307
|
+
* @param {Object} options - Build options
|
|
308
|
+
* @param {string} options.schema - Path to Prisma schema file
|
|
309
|
+
* @param {string} options.output - Output directory for generated models
|
|
310
|
+
* @param {string} options.relationships - Path to relationships.json file
|
|
311
|
+
*/
|
|
312
|
+
async function buildModels(options) {
|
|
313
|
+
const schemaPath = path.resolve(process.cwd(), options.schema);
|
|
314
|
+
const outputBase = path.resolve(process.cwd(), options.output);
|
|
315
|
+
|
|
316
|
+
// If output is "/", use process.cwd() as the base
|
|
317
|
+
const baseDir = options.output === '/' ? process.cwd() : outputBase;
|
|
318
|
+
|
|
319
|
+
// Construct paths
|
|
320
|
+
const srcDir = path.join(baseDir, 'src');
|
|
321
|
+
const modelDir = path.join(srcDir, 'Model');
|
|
322
|
+
const modelJsPath = path.join(srcDir, 'Model.js');
|
|
323
|
+
const rapiddDir = path.join(baseDir, 'rapidd');
|
|
324
|
+
const relationshipsPath = path.join(rapiddDir, 'relationships.json');
|
|
325
|
+
const rlsPath = path.join(rapiddDir, 'rls.js');
|
|
326
|
+
const rapiddJsPath = path.join(rapiddDir, 'rapidd.js');
|
|
327
|
+
const routesDir = path.join(baseDir, 'routes', 'api', 'v1');
|
|
328
|
+
const logsDir = path.join(baseDir, 'logs');
|
|
329
|
+
|
|
330
|
+
console.log('Building Rapidd models...');
|
|
331
|
+
console.log(`Schema: ${schemaPath}`);
|
|
332
|
+
console.log(`Output: ${baseDir}`);
|
|
333
|
+
|
|
334
|
+
// Create logs directory
|
|
335
|
+
if (!fs.existsSync(logsDir)) {
|
|
336
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check if schema file exists
|
|
340
|
+
if (!fs.existsSync(schemaPath)) {
|
|
341
|
+
throw new Error(`Prisma schema file not found at: ${schemaPath}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Run npx prisma generate first
|
|
345
|
+
console.log('\nRunning npx prisma generate...');
|
|
346
|
+
const { execSync } = require('child_process');
|
|
347
|
+
try {
|
|
348
|
+
execSync(`npx prisma generate --schema=${schemaPath}`, {
|
|
349
|
+
stdio: 'inherit',
|
|
350
|
+
cwd: process.cwd()
|
|
351
|
+
});
|
|
352
|
+
console.log('β Prisma client generated successfully\n');
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.warn('β Warning: Failed to generate Prisma client');
|
|
355
|
+
console.warn('Continuing with schema parsing fallback...\n');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Try to use Prisma DMMF first (if prisma generate has been run)
|
|
359
|
+
let parsedData = null;
|
|
360
|
+
const prismaClientPath = path.join(process.cwd(), 'prisma', 'client');
|
|
361
|
+
let usedDMMF = false;
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
parsedData = await parsePrismaDMMF(prismaClientPath);
|
|
365
|
+
if (parsedData) {
|
|
366
|
+
console.log('Using Prisma generated client (DMMF)');
|
|
367
|
+
usedDMMF = true;
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
// Fall back to schema parsing
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// If DMMF parsing failed, parse schema file directly
|
|
374
|
+
if (!parsedData) {
|
|
375
|
+
console.log('Parsing Prisma schema file...');
|
|
376
|
+
parsedData = parsePrismaSchema(schemaPath);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const { models, enums } = parsedData;
|
|
380
|
+
|
|
381
|
+
console.log(`Found ${Object.keys(models).length} models`);
|
|
382
|
+
|
|
383
|
+
// Generate model files
|
|
384
|
+
generateAllModels(models, modelDir, modelJsPath);
|
|
385
|
+
|
|
386
|
+
// Generate src/Model.js (base Model class)
|
|
387
|
+
console.log('\nGenerating src/Model.js...');
|
|
388
|
+
generateBaseModelFile(modelJsPath);
|
|
389
|
+
|
|
390
|
+
// Generate rapidd/rapidd.js
|
|
391
|
+
console.log('Generating rapidd/rapidd.js...');
|
|
392
|
+
generateRapiddFile(rapiddJsPath);
|
|
393
|
+
|
|
394
|
+
// Generate relationships.json
|
|
395
|
+
console.log(`\nGenerating relationships.json...`);
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
if (usedDMMF) {
|
|
399
|
+
await generateRelationshipsFromDMMF(prismaClientPath, relationshipsPath);
|
|
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.');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Generate RLS configuration
|
|
410
|
+
console.log(`\nGenerating RLS configuration...`);
|
|
411
|
+
|
|
412
|
+
// Load relationships for Prisma filter building
|
|
413
|
+
let relationships = {};
|
|
414
|
+
try {
|
|
415
|
+
if (fs.existsSync(relationshipsPath)) {
|
|
416
|
+
relationships = JSON.parse(fs.readFileSync(relationshipsPath, 'utf8'));
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.warn('Could not load relationships.json:', error.message);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
// Parse datasource from Prisma schema to get database URL
|
|
424
|
+
const datasource = parseDatasource(schemaPath);
|
|
425
|
+
|
|
426
|
+
await generateRLS(
|
|
427
|
+
models,
|
|
428
|
+
rlsPath,
|
|
429
|
+
datasource.url,
|
|
430
|
+
datasource.isPostgreSQL,
|
|
431
|
+
options.userTable,
|
|
432
|
+
relationships
|
|
433
|
+
);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.error('Failed to generate RLS:', error.message);
|
|
436
|
+
console.log('Generating permissive RLS fallback...');
|
|
437
|
+
await generateRLS(models, rlsPath, null, false, options.userTable, relationships);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Generate routes
|
|
441
|
+
generateAllRoutes(models, routesDir);
|
|
442
|
+
|
|
443
|
+
return { models, enums };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
module.exports = {
|
|
447
|
+
buildModels
|
|
448
|
+
};
|