├── .eslintrc.json
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .prettierrc.json
├── LICENSE
├── README.md
├── nodemon.json
├── package.json
├── schema.json
├── src
├── assets
│ └── starter
│ │ ├── .eslintrc.json
│ │ ├── .example.env
│ │ ├── .gitignore
│ │ ├── .husky
│ │ └── pre-commit
│ │ ├── Procfile
│ │ ├── package.json
│ │ ├── prisma
│ │ ├── schema.prisma
│ │ └── seed.ts
│ │ ├── src
│ │ ├── config
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── captureError.ts
│ │ │ ├── constants.ts
│ │ │ └── prisma.ts
│ │ └── routes
│ │ │ └── index.ts
│ │ ├── swagger.json
│ │ └── tsconfig.json
├── cli.ts
├── index.ts
└── utils
│ ├── AuthClass.ts
│ ├── EnumClass.ts
│ ├── ModelClass.ts
│ ├── SeedClass.ts
│ ├── common.ts
│ ├── getFields.ts
│ ├── schema.ts
│ └── validateInput.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2021": true,
4 | "node": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "prettier"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaVersion": 12,
14 | "sourceType": "module"
15 | },
16 | "plugins": ["@typescript-eslint"],
17 | "rules": {
18 | "@typescript-eslint/explicit-function-return-type": "off",
19 | "@typescript-eslint/explicit-module-boundary-types": "off"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # app
107 | app
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Abhinandan Sharma
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
CRUDify
11 |
12 |
13 | A
14 | command-line tool available as an NPM package which creates a starter backend project consisting of CRUD API endpoints
15 | and a configured PostgreSQL database (using Prisma ORM) from just an ER Diagram which the user needs to provide in
16 | JSON format.
17 |
18 |
19 | Try it out
20 | ·
21 | Report Bug
22 | ·
23 | Request Feature
24 |
25 |
26 |
27 |
53 |
54 |
55 |
56 | ## About The Project
57 |
58 | CRUDify is a command-line tool to kickstart a backend project by just providing an ER Diagram.
59 |
60 |
61 | The user needs to create a database
62 | schema in JSON format and then install
63 | the package. Next step is to invoke the package
64 | from the command line and pass the
65 | name of the schema file along with it.
66 |
67 |
68 | This creates a backend project with the corresponding database schema file for Prisma ORM. Further, it also contains all the
69 | endpoints for CRUD operations for all
70 | database tables.
71 |
72 |
73 |
74 | ## Installation
75 |
76 | Install the NPM package
77 |
78 | ```
79 | yarn add crudify-dtu
80 | ```
81 |
82 | ```
83 | npm i crudify-dtu
84 | ```
85 |
86 | ## Installation - Local Development
87 |
88 | Install the dependencies
89 |
90 | ```
91 | yarn install
92 | ```
93 |
94 | Build the project in watch mode
95 |
96 | ```
97 | yarn build -w
98 | ```
99 |
100 | In a separate terminal, run the project
101 |
102 | ```
103 | yarn dev
104 | ```
105 |
106 | ## How To Use
107 |
108 | Consider the following ER Diagram
109 |
110 |
111 |
112 |
113 | Shown below will be the corresponding schema for CRUDify
114 |
115 | ```
116 | {
117 | "Models": [
118 | {
119 | "name": "user",
120 | "attributes": {
121 | "StaticFields": [
122 | {
123 | "name": "email",
124 | "type": "String",
125 | "isUnique": true,
126 | "faker": {
127 | "module": "internet",
128 | "method": "email"
129 | }
130 | },
131 | {
132 | "name": "password",
133 | "type": "String",
134 | "toBeHashed": true,
135 | "faker": {
136 | "module": "internet",
137 | "method": "password"
138 | }
139 | },
140 | {
141 | "name": "name",
142 | "type": "String"
143 | }
144 | ],
145 | "RelationalFields": []
146 | }
147 | },
148 | {
149 | "name": "blog",
150 | "attributes": {
151 | "StaticFields": [
152 | {
153 | "name": "title",
154 | "type": "String"
155 | },
156 | {
157 | "name": "content",
158 | "type": "String"
159 | }
160 | ],
161 | "RelationalFields": [
162 | {
163 | "connection": "user",
164 | "foriegnKeyName": "id",
165 | "type": "ONETOMANY"
166 | }
167 | ]
168 | }
169 | },
170 | {
171 | "name": "review",
172 | "attributes": {
173 | "StaticFields": [
174 | {
175 | "name": "title",
176 | "type": "String"
177 | },
178 | {
179 | "name": "content",
180 | "type": "String"
181 | }
182 | ],
183 | "RelationalFields": [
184 | {
185 | "connection": "user",
186 | "foriegnKeyName": "id",
187 | "type": "ONETOMANY"
188 | },
189 | {
190 | "connection": "blog",
191 | "foriegnKeyName": "id",
192 | "type": "ONETOMANY"
193 | }
194 | ]
195 | }
196 | }
197 | ],
198 | "Authentication": {
199 | "model": "user",
200 | "userFieldName": "email",
201 | "passwordFieldName": "password"
202 | }
203 | }
204 | ```
205 |
206 |
Step 1: Create a new folder for your project
207 |
208 |
209 | Step 2: Create your schema as a JSON file
210 |
211 |
212 | Step 3: Install the crudify-dtu NPM package
213 |
214 |
215 | Step 4: CRUDify your ER Diagram using npx crudify-dtu “schema.json” command
216 |
217 |
218 | You can see the equivalent schema created in Prisma ORM in app/prisma/schema.prisma file
219 | This schema is converted into raw SQL queries after setup (after Step 5)
220 |
221 |
222 |
223 | You can see app/src/routes/ contains the APIs for blog, review and user models
224 |
225 |
226 | Step 5: cd into app directory and follow the instructions shown below for setup
227 |
228 |
229 | Create a `.env` file at the root of the app and copy the content of `.example.env` file into it. Then, add your PostgreSQL username and password and replace the database name `starter` with a name of your choice. After creating the `.env` file, run the following commands:
230 |
231 | ```
232 | yarn install
233 | yarn prisma migrate dev
234 | yarn build
235 | yarn dev
236 | ```
237 |
238 | ## Syntax For Creating JSON Schema File
239 |
240 | ```
241 | {
242 | "Models": [
243 | {
244 | "name": "MODEL_NAME",
245 | "softDelete": false/true, // It is an optional field with default value as true
246 | "attributes": {
247 | "StaticFields": [
248 | {
249 | "name": "FIELD_NAME",
250 | "type": "FIELD_TYPE",
251 | "isUnique": true,
252 | "toBeHashed": true,
253 | "faker": {
254 | "module": "MODULE_NAME",
255 | "method": "FUNCTION_NAME"
256 | }
257 | }
258 | ],
259 | "RelationalFields": [
260 | {
261 | "connection": "RELATED_TABLE_NAME",
262 | "foriegnKeyName": "id",
263 | "type": "CONNECTION_TYPE"
264 | }
265 | ]
266 | }
267 | }
268 | ],
269 | "Enums": [
270 | {
271 | "name": "ENUM_NAME",
272 | "fields": ["SAMPLE_FIELD1", "SAMPLE_FIELD2"]
273 | }
274 | ],
275 | "Authentication": {
276 | "model": "YOUR_USER_MODEL_NAME",
277 | "userFieldName": "YOUR_USERNAME_FIELD_NAME",
278 | "passwordFieldName": "YOUR_PASSWORD_FIELD_NAME"
279 | }
280 | }
281 | ```
282 |
283 | **MODEL_NAME:** Name of the table (must be lowercase)
284 |
285 | **softDelete:** False if you don't want soft deletes enabled on a particular model. See more about soft deletes [here](https://en.wiktionary.org/wiki/soft_deletion).
286 |
287 | **StaticFields:** Array of JSON objects with each object representing a non-relational field
288 |
289 | **FIELD_NAME:** Name of the field (must be lowercase)
290 |
291 | **FIELD_TYPE:** Type of the field (can be either `String` , `Boolean` , `Int` , `BigInt` , `Float` , `Decimal` , `DateTime` , `Json`)
292 |
293 | **isUnique:** Boolean that signifies whether the unique constraint should be applied to the field. Defaults to `false`, so can be omitted.
294 |
295 | **toBeHashed:** Boolean that signifies whether the field's value should be hashed before saving to the database. Defaults to `false`, so can be omitted.
296 |
297 | **faker:** Object representing the type of seed (fake) data that should be generated for the field. It is optional
298 |
299 | **module:** Name of the module (e.g. lorem) from https://fakerjs.dev/api/
300 |
301 | **method:** Name of the function to be called for the provided module [e.g. word (for lorem module)] from https://fakerjs.dev/api/
302 |
303 | **RelationalFields:** Array of JSON objects with each object representing a relational field
304 |
305 | **RELATED_TABLE_NAME:** Name of the table to which you want to create a relation to
306 |
307 | **foriegnKeyName:** Name of the field in the `RELATED_TABLE_NAME` table which should be made the foreign key. It should be set as `id` to set the default auto-generated primary key of the `RELATED_TABLE_NAME` table as the foreign key
308 |
309 | **CONNECTION_TYPE:** Can be either `ONETOMANY` or `ONETOONE`. In the case of `ONETOMANY` connection, one record in `RELATED_TABLE_NAME` will be related to many `MODEL_NAME` records
310 |
311 | **Enums:** Specify to use enums in your database. See more about enums and their usage [here](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#defining-enums)
312 |
313 |
314 | **USER AUTHENTICATION DETAILS**
315 |
316 | **Authentication:** An object containing information regarding user authentication. It is optional and should be added only if user authentication API endpoints are required (`login` and `getCurrentUser` currently)
317 |
318 | **model:** Name of the user model defined previously (case-sensitive)
319 |
320 | **userFieldName:** Name of the field in the user model corresponding to `username` (Must be a unique field)
321 |
322 | **passwordFieldName:** Name of the field in the user model corresponding to `password`
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
359 |
360 |
394 |
395 |
404 |
405 |
420 |
421 |
422 |
423 | ## Contributing
424 |
425 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
426 |
427 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
428 | Don't forget to give the project a star! Thanks again!
429 |
430 | 1. Fork the Project
431 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
432 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
433 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
434 | 5. Open a Pull Request
435 |
436 | (back to top)
437 |
438 |
439 |
440 | ## License
441 |
442 | Distributed under the MIT License. See `LICENSE` for more information.
443 |
444 | (back to top)
445 |
446 |
447 |
448 | ## Contact
449 |
450 | Naman Gogia - [Linkedin](https://www.linkedin.com/in/namangogia/) - namangogia2001@gmail.com
451 |
452 | Abhinandan Sharma - [Linkedin](https://www.linkedin.com/in/abhinandan-sharma-dtu/) - abhi.moudgil15@gmail.com
453 |
454 | (back to top)
455 |
456 |
472 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": ["./app/**"]
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crudify-dtu",
3 | "version": "1.2.11",
4 | "main": "dist/index.js",
5 | "repository": "https://github.com/abhi-824/crudify.git",
6 | "author": "Abhi naman",
7 | "license": "MIT",
8 | "scripts": {
9 | "prepare": "husky install",
10 | "build": "yarn tsc",
11 | "dev": "nodemon dist/cli.js \"schema.json\""
12 | },
13 | "dependencies": {
14 | "@prisma/sdk": "^3.13.0",
15 | "chalk": "^4.1.2",
16 | "fs-extra": "^10.0.1",
17 | "joi": "^17.6.0",
18 | "module-alias": "^2.2.2",
19 | "prettier": "^2.6.2",
20 | "typescript": "^4.6.3"
21 | },
22 | "devDependencies": {
23 | "@types/chalk": "^2.2.0",
24 | "@types/fs-extra": "^9.0.13",
25 | "@types/node": "^17.0.24",
26 | "@typescript-eslint/eslint-plugin": "^5.19.0",
27 | "@typescript-eslint/parser": "^5.19.0",
28 | "eslint": "^8.13.0",
29 | "eslint-config-prettier": "^8.5.0",
30 | "husky": "^8.0.0",
31 | "lint-staged": "^12.3.8",
32 | "nodemon": "^2.0.15"
33 | },
34 | "_moduleAliases": {
35 | "@root": "."
36 | },
37 | "lint-staged": {
38 | "*.{ts,js}": "eslint --cache --fix",
39 | "*.{ts,js,css,md}": "prettier --write",
40 | "*.js": "eslint --cache --fix",
41 | "*.{js,css,md}": "prettier --write"
42 | },
43 | "bin": "dist/cli.js"
44 | }
45 |
--------------------------------------------------------------------------------
/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "Models": [
3 | {
4 | "name": "user",
5 | "attributes": {
6 | "StaticFields": [
7 | {
8 | "name": "email",
9 | "type": "String",
10 | "isUnique": true,
11 | "faker": {
12 | "module": "internet",
13 | "method": "email"
14 | }
15 | },
16 | {
17 | "name": "password",
18 | "type": "String",
19 | "toBeHashed": true,
20 | "faker": {
21 | "module": "internet",
22 | "method": "password"
23 | }
24 | },
25 | {
26 | "name": "name",
27 | "type": "String"
28 | },
29 | {
30 | "name": "role",
31 | "type": "Role"
32 | }
33 | ],
34 | "RelationalFields": []
35 | }
36 | },
37 | {
38 | "name": "blog",
39 | "attributes": {
40 | "StaticFields": [
41 | {
42 | "name": "title",
43 | "type": "String",
44 | "defaultValue": "\"Untitled\""
45 | },
46 | {
47 | "name": "content",
48 | "type": "String"
49 | }
50 | ],
51 | "RelationalFields": [
52 | {
53 | "name": "user",
54 | "connection": "user",
55 | "foriegnKeyName": "id",
56 | "type": "ONETOMANY"
57 | }
58 | ]
59 | }
60 | },
61 | {
62 | "name": "review",
63 | "softDelete": false,
64 | "attributes": {
65 | "StaticFields": [
66 | {
67 | "name": "title",
68 | "type": "String"
69 | },
70 | {
71 | "name": "content",
72 | "type": "String"
73 | }
74 | ],
75 | "RelationalFields": [
76 | {
77 | "name": "userA",
78 | "connection": "user",
79 | "foriegnKeyName": "id",
80 | "type": "ONETOMANY"
81 | },
82 | {
83 | "name": "userB",
84 | "connection": "user",
85 | "foriegnKeyName": "id",
86 | "type": "ONETOMANY"
87 | },
88 | {
89 | "name": "blog",
90 | "connection": "blog",
91 | "foriegnKeyName": "id",
92 | "type": "ONETOMANY"
93 | }
94 | ]
95 | }
96 | }
97 | ],
98 | "Enums": [
99 | {
100 | "name": "Role",
101 | "fields": ["USER", "ADMIN"]
102 | }
103 | ],
104 | "Authentication": {
105 | "model": "user",
106 | "userFieldName": "email",
107 | "passwordFieldName": "password"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/assets/starter/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2021": true,
4 | "node": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "prettier"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaVersion": 12,
14 | "sourceType": "module"
15 | },
16 | "plugins": ["@typescript-eslint"],
17 | "rules": {
18 | "@typescript-eslint/explicit-function-return-type": "off",
19 | "@typescript-eslint/explicit-module-boundary-types": "off"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/assets/starter/.example.env:
--------------------------------------------------------------------------------
1 | PORT=5000
2 | HOST=localhost:5000
3 | DATABASE_URL="postgresql://:@localhost:5432/starter"
4 | JWT_SECRET=
--------------------------------------------------------------------------------
/src/assets/starter/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 | .env.production
76 |
77 | # parcel-bundler cache (https://parceljs.org/)
78 | .cache
79 | .parcel-cache
80 |
81 | # Next.js build output
82 | .next
83 | out
84 |
85 | # Nuxt.js build / generate output
86 | .nuxt
87 | dist
88 |
89 | # Gatsby files
90 | .cache/
91 | # Comment in the public line in if your project uses Gatsby and not Next.js
92 | # https://nextjs.org/blog/next-9-1#public-directory-support
93 | # public
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # TernJS port file
108 | .tern-port
109 |
110 | # Stores VSCode versions used for testing VSCode extensions
111 | .vscode-test
112 |
113 | # yarn v2
114 | .yarn/cache
115 | .yarn/unplugged
116 | .yarn/build-state.yml
117 | .yarn/install-state.gz
118 | .pnp.*
--------------------------------------------------------------------------------
/src/assets/starter/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/src/assets/starter/Procfile:
--------------------------------------------------------------------------------
1 | web: npm start
2 |
3 | release: npx prisma migrate deploy
--------------------------------------------------------------------------------
/src/assets/starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starter-backend",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "@prisma/client": "^3.11.1",
8 | "bcrypt": "^5.0.1",
9 | "cors": "^2.8.5",
10 | "dotenv": "^16.0.0",
11 | "express": "^4.17.3",
12 | "faker": "5.5.3",
13 | "joi": "^17.6.0",
14 | "jsonwebtoken": "^8.5.1",
15 | "module-alias": "^2.2.2",
16 | "swagger-ui-express": "^4.5.0",
17 | "node": "^17.7.2",
18 | "prisma": "^3.11.1",
19 | "source-map-support": "^0.5.21",
20 | "ts-node": "^10.7.0",
21 | "typescript": "^4.6.3"
22 | },
23 | "lint-staged": {
24 | "*.{ts,js}": "eslint --cache --fix",
25 | "*.{ts,js,css,md}": "prettier --write",
26 | "*.js": "eslint --cache --fix",
27 | "*.{js,css,md}": "prettier --write"
28 | },
29 | "_moduleAliases": {
30 | "~": "dist/"
31 | },
32 | "prisma": {
33 | "seed": "ts-node prisma/seed.ts"
34 | },
35 | "scripts": {
36 | "prepare": "husky install",
37 | "start": "node dist",
38 | "build": "yarn tsc",
39 | "dev": "nodemon dist",
40 | "ts-node": "ts-node --compiler-options {\\\"module\\\":\\\"commonjs\\\"}",
41 | "migrate": "prisma migrate dev",
42 | "seed": "prisma db seed --preview-feature",
43 | "postinstall": "npm run build"
44 | },
45 | "devDependencies": {
46 | "@faker-js/faker": "^7.6.0",
47 | "@types/bcrypt": "^5.0.0",
48 | "@types/cors": "^2.8.12",
49 | "@types/express": "^4.17.13",
50 | "@types/faker": "5.5.9",
51 | "@types/jsonwebtoken": "^8.5.9",
52 | "@types/node": "^17.0.23",
53 | "@types/swagger-ui-express": "^4.1.3",
54 | "@typescript-eslint/eslint-plugin": "^5.17.0",
55 | "@typescript-eslint/parser": "^5.17.0",
56 | "eslint": "^8.12.0",
57 | "eslint-config-prettier": "^8.5.0",
58 | "husky": "^7.0.4",
59 | "lint-staged": "^12.3.7",
60 | "nodemon": "^2.0.15",
61 | "prettier": "^2.6.1"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/assets/starter/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/starter/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prisma = new PrismaClient();
4 |
5 | export const seed = async () => {
6 | await prisma.$disconnect();
7 | };
8 | seed();
9 | export default seed;
10 |
--------------------------------------------------------------------------------
/src/assets/starter/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { config as dotenvconfig } from "dotenv";
2 |
3 | dotenvconfig();
4 |
5 | export const config = {
6 | PORT: process.env.PORT || 5000,
7 | JWT_SECRET: process.env.JWT_SECRET || "@this1is2a3random4string@",
8 | };
9 |
10 | export default config;
11 |
--------------------------------------------------------------------------------
/src/assets/starter/src/index.ts:
--------------------------------------------------------------------------------
1 | import "module-alias/register";
2 | import "source-map-support/register";
3 | import app from "./app";
4 | import config from "./config";
5 |
6 | app.listen(config.PORT, () => {
7 | console.log(`Server started on http://localhost:${config.PORT}/`);
8 | });
9 |
--------------------------------------------------------------------------------
/src/assets/starter/src/lib/captureError.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Request, Response } from "express";
2 |
3 | export const ce = (fn) => {
4 | return async (req: Request, res: Response, next: NextFunction) => {
5 | try {
6 | await fn(req, res, next);
7 | } catch (e) {
8 | next(e);
9 | }
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/assets/starter/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const softDeletionExcludedModels: Array = [];
--------------------------------------------------------------------------------
/src/assets/starter/src/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import {softDeletionExcludedModels} from "./constants"
3 | export const prisma = new PrismaClient();
4 |
5 |
6 | prisma.$use(async (params, next) => {
7 | if (params.action === "findUnique" || params.action === "findFirst") {
8 | // Change to findFirst - you cannot filter
9 | // by anything except ID / unique with findUnique
10 | params.action = "findFirst";
11 | // Add 'deleted' filter
12 | // ID filter maintained
13 | params.args.where["deleted"] = false;
14 | }
15 | if (params.action === "findMany") {
16 | // Find many queries
17 | if (params.args.where) {
18 | if (params.args.where.deleted == undefined) {
19 | // Exclude deleted records if they have not been explicitly requested
20 | params.args.where["deleted"] = false;
21 | }
22 | } else {
23 | params.args["where"] = { deleted: false };
24 | }
25 | }
26 | return next(params);
27 | });
28 |
29 | prisma.$use(async (params, next) => {
30 | if (params.action == "update") {
31 | // Change to updateMany - you cannot filter
32 | // by anything except ID / unique with findUnique
33 | params.action = "updateMany";
34 | // Add 'deleted' filter
35 | // ID filter maintained
36 | params.args.where["deleted"] = false;
37 | }
38 | if (params.action == "updateMany") {
39 | if (params.args.where != undefined) {
40 | params.args.where["deleted"] = false;
41 | } else {
42 | params.args["where"] = { deleted: false };
43 | }
44 | }
45 | return next(params);
46 | });
47 |
48 | prisma.$use(async (params, next) => {
49 | // Check incoming query type
50 | if (params.model && !softDeletionExcludedModels.includes(params.model)) {
51 | if (params.action == "delete") {
52 | // Delete queries
53 | // Change action to an update
54 | params.action = "update";
55 | params.args["data"] = { deleted: true };
56 | }
57 | if (params.action == "deleteMany") {
58 | // Delete many queries
59 | params.action = "updateMany";
60 | if (params.args.data != undefined) {
61 | params.args.data["deleted"] = true;
62 | } else {
63 | params.args["data"] = { deleted: true };
64 | }
65 | }
66 | }
67 | return next(params);
68 | });
69 | export function exclude(
70 | elem: T,
71 | ...keys: Key[]
72 | ): Omit {
73 | for (const key of keys) {
74 | delete elem[key];
75 | }
76 | return elem;
77 | }
78 | export default prisma;
79 |
--------------------------------------------------------------------------------
/src/assets/starter/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, Router } from "express";
2 |
3 | const router = Router();
4 |
5 | router.get("/", (req: Request, res: Response) => {
6 | res.send("Hey there, this is the api route");
7 | });
8 |
9 | export default router;
10 |
--------------------------------------------------------------------------------
/src/assets/starter/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "description": "Change this description yourself in swagger.json",
5 | "version": "1.0.0",
6 | "title": "Example Title",
7 | "contact": {
8 | "email": "crudify@gmail.com"
9 | },
10 | "license": {
11 | "name": "Apache 2.0",
12 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
13 | }
14 | },
15 | "schemes": ["http"],
16 | "host": "localhost:5000",
17 | "basePath": "/api",
18 | "paths" : {
19 | },
20 | "definitions": {
21 | "DeleteResponse":{
22 | "type": "object",
23 | "properties": {
24 | "data": {
25 | "type": "string"
26 | }
27 | }
28 | },
29 | "InvalidResponse": {
30 | "type": "object",
31 | "properties": {
32 | "data": {
33 | "type": "string"
34 | }
35 | }
36 |
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/assets/starter/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | "sourceMap": true /* Generates corresponding '.map' file. */,
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "./dist/" /* Redirect output structure to the directory. */,
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true /* Enable all strict type-checking options. */,
29 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
45 |
46 | /* Module Resolution Options */
47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
48 | "baseUrl": "./src" /* Base directory to resolve non-absolute module names. */,
49 | "paths": {
50 | "~/*": ["./*"]
51 | } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
53 | // "typeRoots": [], /* List of folders to include type definitions from. */
54 | // "types": [], /* Type declaration files to be included in compilation. */
55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
56 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
59 |
60 | /* Source Map Options */
61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
65 |
66 | /* Experimental Options */
67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
69 |
70 | /* Advanced Options */
71 | "skipLibCheck": true /* Skip type checking of declaration files. */,
72 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
73 | },
74 | "include": ["./src"]
75 | }
76 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import crudify from "./index";
4 | import * as path from "path";
5 | import { exec } from "child_process";
6 | import chalk from "chalk";
7 |
8 | async function main() {
9 | const schemaFileName = path.join(process.cwd(), process.argv[2].toString());
10 | const data = await import(schemaFileName);
11 |
12 | const error = await crudify(data);
13 | if (error) {
14 | console.log(chalk.red(error));
15 | return;
16 | }
17 | console.log(chalk.italic.underline.bold("Formatting your code"));
18 |
19 | exec('prettier --write "app"', (error, stdout, stderr) => {
20 | if (error) {
21 | console.log(chalk.red(`error: ${error.message}`));
22 | return;
23 | }
24 | if (stderr) {
25 | console.log(chalk.red(`stderr: ${stderr}`));
26 | return;
27 | }
28 | // console.log(`stdout: ${stdout}`);
29 | console.log(
30 | chalk.greenBright("Your app can be found at app/ folder. Have fun!")
31 | );
32 | });
33 | }
34 |
35 | main();
36 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import * as path from "path";
3 | import { validateInput } from "./utils/validateInput";
4 | import { Model, RelationalField, StaticField } from "./utils/ModelClass";
5 | import { Authentication } from "./utils/AuthClass";
6 | import { SeedDataGeneration } from "./utils/SeedClass";
7 | import { getRelationalFields, getStaticFields } from "./utils/getFields";
8 | import { formatSchema } from "@prisma/sdk";
9 | import { Enum } from "./utils/EnumClass";
10 | import chalk from "chalk";
11 |
12 | export default async function crudify(data: any) {
13 | // Loading the user schema
14 | console.log("Parsing your ER diagram");
15 |
16 | const { error } = validateInput(data);
17 | if (error) return error;
18 | // Proccessing database models
19 | const dataModels = data.Models;
20 | const dataEnums = data.Enums || [];
21 |
22 | const enums: Array = [];
23 |
24 | for (const dataEnum of dataEnums) {
25 | const _enum: Enum = new Enum(dataEnum.name, dataEnum.fields);
26 | enums.push(_enum);
27 | }
28 | for (const _enum of enums) {
29 | _enum.generatePrismaModel();
30 | }
31 |
32 | const models: Array = [];
33 | const softDeletionExcludedModels: Array = [];
34 |
35 | for (const dataModel of dataModels) {
36 | const model: Model = new Model(dataModel.name, dataModel.softDelete);
37 | if (!model.softDelete) softDeletionExcludedModels.push(model.name);
38 | const staticFields: Array = getStaticFields(dataModel);
39 | const relationalFields: Array =
40 | getRelationalFields(dataModel);
41 |
42 | model.attributes = {
43 | relationalField: relationalFields,
44 | staticField: staticFields,
45 | };
46 |
47 | models.push(model);
48 | }
49 |
50 | for (const model of models) {
51 | model.restructure(models);
52 | model.generateRoutes();
53 | model.generateUserInputValidator(enums);
54 | model.generateRouter();
55 | model.generateDocString();
56 | }
57 |
58 | let prismaSchema = `datasource db {
59 | provider = "postgresql"
60 | url = env("DATABASE_URL")
61 | }
62 |
63 | generator client {
64 | provider = "prisma-client-js"
65 | }
66 | `;
67 |
68 | let swaggerDocPaths = "",
69 | swaggerDocDefinitions = "";
70 | for (const _enum of enums) {
71 | prismaSchema += _enum.prismaModel;
72 | }
73 | for (const model of models) {
74 | model.generateSchema();
75 | prismaSchema += model.prismaModel;
76 | swaggerDocPaths += model.apiDocPathString;
77 | swaggerDocDefinitions += model.apiDocDefinitionString;
78 | }
79 |
80 | console.log("Brace yourself, brewing your backend...");
81 |
82 | // Duplicating the starter backend template
83 | const sourceFolderName = path.join(__dirname, "../src/assets/starter");
84 | const destFolderName = path.join(process.cwd(), "/app");
85 |
86 | await fse.copy(sourceFolderName, destFolderName);
87 |
88 | // Writing prisma schema in the output
89 | const schemaPath = path.join(process.cwd(), "/app/prisma/schema.prisma");
90 | formatSchema({
91 | schema: prismaSchema,
92 | }).then(async (formattedPrismaSchema: string) => {
93 | await fse.outputFile(schemaPath, formattedPrismaSchema);
94 | });
95 |
96 | // Writing each model's CRUD api endpoints to the output
97 | for (const model of models) {
98 | const schemaPath = path.join(
99 | process.cwd(),
100 | `/app/src/routes/${model.name}/`
101 | );
102 | const indexPath = schemaPath + "index.ts";
103 | const controllerPath = schemaPath + "controller.ts";
104 | const inputValidatorPath = schemaPath + "schema.ts";
105 | await fse.outputFile(controllerPath, model.controllerString);
106 | await fse.outputFile(indexPath, model.routerString);
107 | await fse.outputFile(inputValidatorPath, model.validationString);
108 | }
109 |
110 | // Creating the model router ./src/routes/index.ts
111 | const routerIndexString = `
112 | import { Request, Response, Router } from 'express'
113 | ${models
114 | .map((model) => {
115 | return `import ${model.name}Router from './${model.name}'\n`;
116 | })
117 | .join("")}
118 |
119 | const router = Router()
120 |
121 | ${models
122 | .map((model) => {
123 | return `router.use('/${model.name}', ${model.name}Router)\n`;
124 | })
125 | .join("")}
126 |
127 | router.get('/', (req: Request, res: Response) => {
128 | res.send('hello world')
129 | })
130 |
131 | export default router
132 | `;
133 |
134 | const routerIndexPath = path.join(process.cwd(), `/app/src/routes/index.ts`);
135 | await fse.outputFile(routerIndexPath, routerIndexString);
136 |
137 | // Creating the primary router ./src/app.ts
138 | const appRouterString = `import Express from "express";
139 | import cors from "cors";
140 | import config from "./config";
141 | import apiRouter from "./routes";
142 | import * as path from "path";
143 | import swaggerUi from "swagger-ui-express";
144 | import fs from 'fs'
145 | ${data.Authentication ? 'import authRouter from "./routes/auth";' : ""}
146 |
147 | export const app = Express();
148 | const swaggerDocument = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'swagger.json'), 'utf-8'))
149 | swaggerDocument.host=process.env.HOST
150 | app.use(
151 | cors({
152 | origin: "*",
153 | credentials: true,
154 | })
155 | );
156 |
157 | app.use(Express.json());
158 |
159 | app.use("/api", apiRouter);
160 | ${data.Authentication ? 'app.use("/auth", authRouter);' : ""}
161 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
162 |
163 | app.get("/", (req, res) => {
164 | res.send(
165 | 'hello there, see the documentation here: Link'
166 | );
167 | });
168 | export default app;
169 | `;
170 |
171 | const appRouterPath = path.join(process.cwd(), `/app/src/app.ts`);
172 | await fse.outputFile(appRouterPath, appRouterString);
173 |
174 | const swaggerDocString = `{
175 | "swagger": "2.0",
176 | "info": {
177 | "description": "Change this description yourself in swagger.json",
178 | "version": "1.0.0",
179 | "title": "Example Title",
180 | "contact": {
181 | "email": "crudify@gmail.com"
182 | },
183 | "license": {
184 | "name": "Apache 2.0",
185 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
186 | }
187 | },
188 | "schemes": ["http"],
189 | "host": "localhost:5000",
190 | "basePath": "/api",
191 | "paths" : {
192 | ${swaggerDocPaths}
193 | },
194 | "definitions": {
195 | ${swaggerDocDefinitions}
196 | "DeleteResponse":{
197 | "type": "object",
198 | "properties": {
199 | "data": {
200 | "type": "string"
201 | }
202 | }
203 | },
204 | "InvalidResponse": {
205 | "type": "object",
206 | "properties": {
207 | "data": {
208 | "type": "string"
209 | }
210 | }
211 |
212 | }
213 | }
214 | }`;
215 |
216 | console.log(chalk.white("Preparing APIs and getting the API docs ready"));
217 | const swaggerJsonPath = path.join(process.cwd(), `/app/swagger.json`);
218 | await fse.outputFile(swaggerJsonPath, swaggerDocString);
219 |
220 | // Authentication
221 | if (data.Authentication) {
222 | const { model, userFieldName, passwordFieldName } = data.Authentication;
223 | new Authentication(model, userFieldName, passwordFieldName);
224 | }
225 |
226 | const constantsFileContent = `export const softDeletionExcludedModels: Array = [${softDeletionExcludedModels.map(
227 | (model) => {
228 | return `"${model}"`;
229 | }
230 | )}]; `;
231 | const constantsPath = path.join(process.cwd(), `/app/src/lib/constants.ts`);
232 | await fse.outputFile(constantsPath, constantsFileContent);
233 |
234 | // Seed File Generation
235 | console.log(chalk.white("Laying the groundwork for seeding your database"));
236 | new SeedDataGeneration(models, enums);
237 | }
238 |
--------------------------------------------------------------------------------
/src/utils/AuthClass.ts:
--------------------------------------------------------------------------------
1 | import { joiMapping } from "./schema";
2 | import * as path from "path";
3 | import * as fse from "fs-extra";
4 |
5 | export class Authentication {
6 | model: string;
7 | userFieldName: string;
8 | passwordFieldName: string;
9 | routerString = "";
10 | controllerString = "";
11 | validationString = "";
12 | routerPath = path.join(process.cwd(), "/app/src/routes/auth/index.ts");
13 | controllerPath = path.join(
14 | process.cwd(),
15 | "/app/src/routes/auth/controller.ts"
16 | );
17 | inputValidatorPath = path.join(
18 | process.cwd(),
19 | "/app/src/routes/auth/schema.ts"
20 | );
21 |
22 | constructor(model: string, userFieldName: string, passwordFieldName: string) {
23 | this.model = model;
24 | this.userFieldName = userFieldName;
25 | this.passwordFieldName = passwordFieldName;
26 |
27 | this.generateRoutes();
28 | this.generateUserInputValidator();
29 | this.generateRouter();
30 | }
31 |
32 | generateRouter() {
33 | this.routerString = `
34 | import { Router } from "express";
35 | import { ce } from "~/lib/captureError";
36 | import { handleLogin, getCurrentUser } from "./controller";
37 |
38 | export const router = Router();
39 |
40 | router.post("/login", ce(handleLogin));
41 | router.get("/me", ce(getCurrentUser));
42 |
43 | export default router;
44 | `;
45 |
46 | fse.outputFileSync(this.routerPath, this.routerString);
47 | }
48 |
49 | generateRoutes() {
50 | this.controllerString = `
51 | import { Request, Response } from "express";
52 | import prisma from "~/lib/prisma";
53 | import { schema } from "./schema";
54 | import * as bcrypt from "bcrypt";
55 | import * as jwt from "jsonwebtoken";
56 | import config from "../../config";
57 |
58 | export const handleLogin = async (req: Request, res: Response) => {
59 | const { error } = schema.validate(req.body);
60 | if (!error) {
61 | const ${this.userFieldName} = req.body.${this.userFieldName};
62 | if (${this.userFieldName}.length === 0)
63 | return res.status(400).json({ data: "Invalid ${this.userFieldName}" });
64 |
65 | const request = await prisma.${this.model}.findUnique({
66 | where: { ${this.userFieldName}: ${this.userFieldName} },
67 | });
68 |
69 | if (!request) return res.status(404).json({ data: "${this.model} not found" });
70 |
71 | const match = await bcrypt.compare(req.body.password, request.password);
72 | if (match) {
73 | //login user
74 | const secret = config.JWT_SECRET;
75 | const payload = {
76 | ${this.userFieldName}: request.${this.userFieldName},
77 | };
78 | const token = jwt.sign(payload, secret, { expiresIn: "24h" });
79 | res.status(200).json({
80 | token,
81 | message: "Credentials are correct!, user is logged in.",
82 | });
83 | } else {
84 | res.status(400).json({
85 | message:
86 | "User Credentials are invalid, Kindly redirect to login page again",
87 | });
88 | }
89 | }
90 | };
91 |
92 | export const getCurrentUser = async (req: Request, res: Response) => {
93 | let token: string;
94 | if (
95 | req.headers.authorization &&
96 | req.headers.authorization.startsWith("Bearer")
97 | ) {
98 | try {
99 | token = req.headers.authorization.split(" ")[1];
100 | const secret = config.JWT_SECRET;
101 |
102 | const decoded: any = jwt.verify(token, secret);
103 |
104 | const ${this.model} = await prisma.${this.model}.findUnique({
105 | where: {
106 | ${this.userFieldName}: decoded.${this.userFieldName},
107 | },
108 | });
109 |
110 | if (${this.model}?.password) {
111 | ${this.model}.password = "";
112 | }
113 |
114 | return res.status(200).json({ data: ${this.model} });
115 | } catch (error) {
116 | console.error(error);
117 | res.status(401);
118 | throw new Error("Not authorized, token failed");
119 | }
120 | }
121 |
122 | return res.status(400).json({ data: "Invalid token" });
123 | };
124 | `;
125 |
126 | fse.outputFileSync(this.controllerPath, this.controllerString);
127 | }
128 |
129 | generateUserInputValidator() {
130 | this.validationString = `
131 | import Joi from 'joi'
132 | export const schema = Joi.object().keys({
133 | ${[
134 | { name: this.userFieldName, type: "String" },
135 | { name: this.passwordFieldName, type: "String" },
136 | ]
137 | .map((fieldData) => `${fieldData.name}: ${joiMapping[fieldData.type]},`)
138 | .join("\n")}
139 | })
140 | `;
141 |
142 | fse.outputFileSync(this.inputValidatorPath, this.validationString);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/utils/EnumClass.ts:
--------------------------------------------------------------------------------
1 | export interface field {
2 | name: string;
3 | }
4 | export class Enum {
5 | name: string;
6 | fields: Array;
7 | constructor(name: string, fields: Array) {
8 | this.name = name;
9 | this.fields = fields;
10 | }
11 | prismaModel = "";
12 | generatePrismaModel() {
13 | this.prismaModel = `enum ${this.name} {
14 | ${this.fields.map((field) => {
15 | return `${field}\n`;
16 | }).join("")}
17 | }`;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/ModelClass.ts:
--------------------------------------------------------------------------------
1 | import { convertToUpperCamelCase } from "./common";
2 | import { joiMapping } from "./schema";
3 | import chalk from "chalk";
4 | import { Enum } from "./EnumClass";
5 | export enum type {
6 | ONETOONE,
7 | ONETOMANY,
8 | }
9 |
10 | export interface StaticField {
11 | name: string;
12 | type: string;
13 | isUnique?: boolean;
14 | toBeHashed?: boolean;
15 | defaultValue?: string;
16 | faker?: {
17 | module: string;
18 | method: string;
19 | };
20 | }
21 |
22 | export interface RelationalField {
23 | name: string;
24 | connection: string;
25 | foreignKey: string;
26 | type: type;
27 | }
28 |
29 | export interface Attributes {
30 | staticField: Array;
31 | relationalField: Array;
32 | }
33 | export interface MapPrismaToSwagger {
34 | [key: string]: string;
35 | }
36 | export class Model {
37 | name: string;
38 | softDelete = true;
39 | attributes: Attributes = { staticField: [], relationalField: [] };
40 | prismaModelArray: Array = [];
41 | prismaModel = "";
42 | routerString = "";
43 | controllerString = "";
44 | validationString = "";
45 | apiDocPathString = "";
46 | apiDocDefinitionString = "";
47 | private mapPrismaToSwagger: MapPrismaToSwagger = {
48 | String: "string",
49 | Int: "integer",
50 | };
51 |
52 | constructor(name: string, softDelete = true) {
53 | this.name = name;
54 | this.softDelete = softDelete;
55 | }
56 |
57 | staticFieldNames() {
58 | const staticFieldNamesArray: Array = [];
59 | for (const staticField of this.attributes.staticField) {
60 | staticFieldNamesArray.push(staticField.name);
61 | }
62 | return staticFieldNamesArray;
63 | }
64 |
65 | relationalFieldNames() {
66 | const relationalFieldNamesArray: Array = [];
67 | for (const relationalField of this.attributes.relationalField) {
68 | relationalFieldNamesArray.push(`${relationalField.name}`);
69 | }
70 | return relationalFieldNamesArray;
71 | }
72 |
73 | private staticFieldConversion() {
74 | for (const staticField of this.attributes.staticField) {
75 | const defaultValue = `@default(${staticField.defaultValue})`;
76 | if (staticField.defaultValue && staticField.isUnique) {
77 | console.log(
78 | chalk.yellow(
79 | `WARNING: You have given a default value to a unique field in ${this.name} model for ${staticField.name} attribute. It may give you error in future!`
80 | )
81 | );
82 | }
83 | this.prismaModelArray.push(
84 | `${staticField.name} ${staticField.type} ${
85 | staticField.isUnique ? "@unique" : ""
86 | } ${staticField.defaultValue ? defaultValue : ""} \n`
87 | );
88 | }
89 | this.prismaModelArray.push("deleted Boolean @default(false)\n");
90 | }
91 |
92 | private relationalFieldConversion(models: Array) {
93 | for (const relationalField of this.attributes.relationalField) {
94 | this.prismaModelArray.push(`${relationalField.name}Id Int `);
95 | this.prismaModelArray.push(
96 | `\n ${relationalField.name} ${
97 | relationalField.connection
98 | } @relation(name: "${relationalField.connection.toLowerCase()}_${
99 | relationalField.name
100 | }Id_${this.name}", fields: [${
101 | relationalField.name
102 | }Id], references: [id], onDelete: Cascade)\n`
103 | );
104 | const connectedModel: Model | undefined = models.find(
105 | (model) => model.name === relationalField.connection
106 | );
107 |
108 | let connectionCount = 0;
109 | this.attributes.relationalField.forEach((field) => {
110 | if (relationalField.connection === field.connection) connectionCount++;
111 | });
112 |
113 | if (connectedModel) {
114 | let oneSideConnectionString = `${this.name} ${
115 | relationalField.type === ("ONETOONE" as unknown as type) ? "?" : "[]"
116 | } @relation(name: "${relationalField.connection.toLowerCase()}_${
117 | relationalField.name
118 | }Id_${this.name}")\n`;
119 |
120 | if (connectionCount > 1)
121 | oneSideConnectionString =
122 | `${this.name}_${relationalField.name}` +
123 | " " +
124 | oneSideConnectionString;
125 | else
126 | oneSideConnectionString =
127 | `${this.name}` + " " + oneSideConnectionString;
128 |
129 | connectedModel.prismaModelArray.push(oneSideConnectionString);
130 | }
131 | }
132 | }
133 |
134 | restructure(models: Array) {
135 | this.staticFieldConversion();
136 | this.relationalFieldConversion(models);
137 | }
138 |
139 | generateSchema() {
140 | this.prismaModel = `model ${this.name} {
141 | id Int @id @default(autoincrement())\n
142 | `;
143 | for (const schemaString of this.prismaModelArray) {
144 | this.prismaModel += schemaString;
145 | }
146 | this.prismaModel += "createdAt DateTime @default(now())\n";
147 | this.prismaModel += "updatedAt DateTime @default(now())";
148 | this.prismaModel += "\n}\n";
149 | }
150 |
151 | generateRouter() {
152 | const modelName = convertToUpperCamelCase(this.name);
153 | this.routerString = `
154 | import { Router } from "express";
155 | import { ce } from "~/lib/captureError";
156 | import {
157 | handleCreate${modelName},
158 | handleDelete${modelName},
159 | handleGetAll${modelName}s,
160 | handleGet${modelName}ById,
161 | handleUpdate${modelName}ById,
162 | } from "./controller";
163 |
164 | export const router = Router();
165 |
166 | //CRUD routes
167 | router.get("/", ce(handleGetAll${modelName}s));
168 | router.get("/:id", ce(handleGet${modelName}ById));
169 | router.post("/", ce(handleCreate${modelName}));
170 | router.patch("/:id", ce(handleUpdate${modelName}ById));
171 | router.delete("/:id", ce(handleDelete${modelName}));
172 |
173 | export default router;
174 | `;
175 | }
176 |
177 | generateRoutes() {
178 | const modelName = convertToUpperCamelCase(this.name);
179 |
180 | this.controllerString = `
181 | import { Prisma, ${this.name} } from ".prisma/client";
182 | import { Request, Response } from "express";
183 | import prisma from "~/lib/prisma";
184 | import { schema } from "./schema";
185 | import { exclude } from "~/lib/prisma";
186 | import * as bcrypt from "bcrypt";
187 |
188 | export const handleCreate${modelName} = async (req: Request, res: Response) => {
189 | const { error } = schema.validate(req.body);
190 | if (!error) {
191 |
192 | const { ${[
193 | ...this.staticFieldNames(),
194 | ...this.relationalFieldNames(),
195 | ].join(",")} } = req.body;
196 |
197 | ${this.relationalFieldNames()
198 | .map((relationalField) => {
199 | return `const ${relationalField}ToBeConnected = await prisma.${relationalField}.findUnique({
200 | where: { id: ${relationalField} },
201 | });
202 |
203 | if (!${relationalField}ToBeConnected)
204 | return res.status(400).json({ data: "${convertToUpperCamelCase(
205 | relationalField
206 | )} not found" });
207 | `;
208 | })
209 | .join("")}
210 |
211 | const new${modelName}Object = {
212 | ${this.attributes.staticField
213 | .map((field) => {
214 | if (field.toBeHashed === true)
215 | return `${field.name}: await bcrypt.hash(${field.name}, 10)`;
216 |
217 | return field.name;
218 | })
219 | .join(",")},
220 | ${this.relationalFieldNames()
221 | .map((relationalFieldName) => {
222 | return `${relationalFieldName}: { connect: { id: ${relationalFieldName} } },`;
223 | })
224 | .join("")}
225 |
226 | };
227 | const ${this.name} = await prisma.${this.name}.create({
228 | data: new${modelName}Object,
229 | });
230 | const ${this.name}WithoutDeleted = exclude(${this.name}, 'deleted')
231 | return res.json({ data: ${this.name}WithoutDeleted });
232 | }
233 | return res.status(500).json({ data: error.details[0].message });
234 | };
235 |
236 | export const handleDelete${modelName} = async (
237 | req: Request<{ id: string }>,
238 | res: Response
239 | ) => {
240 | const ${this.name}Id = Number(req.params.id);
241 | if (!${this.name}Id) return res.status(400).json({ data: "Invalid ID" });
242 |
243 | const ${this.name} = await prisma.${this.name}.findUnique({
244 | where: { id: ${this.name}Id },
245 | });
246 |
247 | if (!${
248 | this.name
249 | }) return res.status(404).json({ data: "${modelName} Not Found" });
250 |
251 | await prisma.${this.name}.delete({
252 | where: {
253 | id: ${this.name}Id,
254 | },
255 | });
256 |
257 | return res.status(200).json({ data: "Successfully Deleted!" });
258 | };
259 |
260 | export const handleGetAll${convertToUpperCamelCase(
261 | modelName
262 | )}s = async (req: Request, res: Response) => {
263 | const skip = Number(req.query.skip) || 0;
264 | const take = Number(req.query.take) || 10;
265 |
266 | const ${this.name}s = await prisma.${this.name}.findMany({
267 | skip: skip,
268 | take: take,
269 | });
270 | const ${this.name}sWithoutDeleted:Array>=[];
271 | for(const ${this.name} in ${this.name}s) {
272 | ${this.name}sWithoutDeleted.push(exclude( ${this.name}s[ ${
273 | this.name
274 | }],"deleted"));
275 | }
276 | return res.json({ data: ${this.name}sWithoutDeleted });
277 | };
278 |
279 | export const handleGet${modelName}ById = async (
280 | req: Request<{ id: string }>,
281 | res: Response
282 | ) => {
283 | const ${this.name}Id = Number(req.params.id);
284 | if (isNaN(${this.name}Id))
285 | return res.status(400).json({ data: "Invalid Id" });
286 |
287 | const ${this.name} = await prisma.${this.name}.findUnique({
288 | where: { id: ${this.name}Id },
289 | });
290 | if (!${this.name})
291 | return res.status(404).json({ data: "${modelName} not found" });
292 | const ${this.name}WithoutDeleted = exclude(${this.name}, "deleted");
293 | return res.json({ data: ${this.name}WithoutDeleted });
294 | };
295 |
296 | export const handleUpdate${modelName}ById = async (
297 | req: Request<{ id: string }>,
298 | res: Response
299 | ) => {
300 | const ${this.name}Id = Number(req.params.id);
301 | const allowedUpdateFields: Array = [
302 | "${[...this.staticFieldNames(), ...this.relationalFieldNames()].join(
303 | `","`
304 | )}"
305 | ];
306 |
307 | const updates = Object.keys(req.body);
308 |
309 | const updateObject: Prisma.${this.name}UpdateInput = {};
310 |
311 | for (const update of updates) {
312 | if (!allowedUpdateFields.includes(update as keyof Prisma.${
313 | this.name
314 | }UpdateInput))
315 | return res.status(400).json({ data: "Invalid Arguments" });
316 |
317 | if (["${this.relationalFieldNames().join(`","`)}"].includes(update)) {
318 | const entityConnection = {
319 | connect: { id: req.body[update] },
320 | };
321 | const elem = await prisma[update].findUnique({
322 | where: { id: req.body[update] },
323 | });
324 | if (!elem) return res.status(400).json({ data: \`\${update} not found\` });
325 | updateObject[update] = entityConnection;
326 | } else updateObject[update] = req.body[update];
327 | }
328 |
329 | const ${this.name}ToBeUpdated = await prisma.${this.name}.findUnique({
330 | where: { id: ${this.name}Id },
331 | });
332 | if (!${this.name}ToBeUpdated)
333 | return res.status(404).json({ data: "${modelName} Not Found" });
334 |
335 | updateObject.updatedAt = new Date();
336 | await prisma.${this.name}.update({
337 | where: {
338 | id: ${this.name}Id,
339 | },
340 | data: updateObject,
341 | });
342 | const ${this.name} = await prisma.${this.name}.findUnique({
343 | where: { id: ${this.name}Id },
344 | });
345 | if (!${this.name}) return res.status(404).json({ data: "${
346 | this.name
347 | } not found" });
348 | const ${this.name}WithoutDeleted = exclude(${this.name}, "deleted");
349 | return res.json({ data: ${this.name}WithoutDeleted });
350 | };
351 | `;
352 | }
353 |
354 | generateUserInputValidator(enums: Array) {
355 | enums.map((enumObj) => {
356 | joiMapping[enumObj.name] = `Joi.string().valid(${enumObj.fields
357 | .map((enumVal) => `"${enumVal}"`)
358 | .join(",")})`;
359 | });
360 | this.validationString = `
361 | import Joi from 'joi'
362 | export const schema = Joi.object().keys({
363 | ${[
364 | ...this.attributes.staticField.map((field) => {
365 | return { name: field.name, type: field.type };
366 | }),
367 | ...this.attributes.relationalField.map((field) => {
368 | return { name: field.connection, type: "Int" };
369 | }),
370 | ]
371 | .map((fieldData) => {
372 | return `${fieldData.name}: ${joiMapping[fieldData.type]},`;
373 | })
374 | .join("\n")}
375 | })
376 | `;
377 | }
378 | generateDocString() {
379 | this.apiDocPathString = `"/${this.name}" : {
380 | "get" : {
381 | "summary" : "Get all the ${this.name}s",
382 | "description": "Get all the ${this.name}s",
383 | "produces": ["application/json"],
384 | "parameters": [],
385 | "tags":["${this.name}"],
386 | "responses": {
387 | "200": {
388 | "description": "successful operation",
389 | "schema": {
390 | "type": "array",
391 | "items": {
392 | "$ref": "#/definitions/${this.name}Response"
393 | }
394 | }
395 | }
396 | }
397 | },
398 | "post" : {
399 | "summary" : "Save the ${this.name}",
400 | "description": "Save the ${this.name}",
401 | "produces": ["application/json"],
402 | "consumes": ["application/json"],
403 | "tags":["${this.name}"],
404 | "parameters": [
405 | {
406 | "in": "body",
407 | "name": "body",
408 | "description": "${this.name} object",
409 | "required": true,
410 | "schema": {
411 | "type": "object",
412 | "$ref": "#/definitions/${this.name}"
413 | }
414 | }
415 | ],
416 | "responses": {
417 | "200": {
418 | "description": "successful operation",
419 | "schema": {
420 | "type": "object",
421 | "properties": {
422 | "data":{
423 | "type":"object",
424 | "$ref": "#/definitions/${this.name}Response"
425 | }
426 | }
427 | }
428 | },
429 | "400": {
430 | "description": "Invalid request body",
431 | "schema": {
432 | "$ref": "#/definitions/InvalidResponse"
433 | }
434 | }
435 | }
436 | }
437 | },
438 | "/${this.name}/{id}" : {
439 | "get" : {
440 | "summary" : "Get ${this.name} by id",
441 | "description": "Get ${this.name} by id",
442 | "produces": ["application/json"],
443 | "parameters": [
444 | {
445 | "name": "id",
446 | "in": "path",
447 | "description": "${this.name} id that needs to be fetched",
448 | "required": true,
449 | "type": "string"
450 | },
451 | ],
452 | "tags":["${this.name}"],
453 | "responses": {
454 | "200": {
455 | "description": "successful operation",
456 | "schema": {
457 | "type": "object",
458 | "properties": {
459 | "data":{
460 | "type":"object",
461 | "$ref": "#/definitions/${this.name}Response"
462 | }
463 | }
464 | }
465 | },
466 | "400": {
467 | "description": "Invalid status value",
468 | "schema": {
469 | "$ref": "#/definitions/InvalidResponse"
470 | }
471 | },
472 | "404":{
473 | "description": "Couldn't Find",
474 | "schema": {
475 | "$ref": "#/definitions/InvalidResponse"
476 | }
477 | }
478 | }
479 | },
480 | "patch" : {
481 | "summary" : "Update the ${this.name}",
482 | "description": "Update the ${this.name}",
483 | "produces": ["application/json"],
484 | "tags":["${this.name}"],
485 | "parameters": [
486 | {
487 | "name": "id",
488 | "in": "path",
489 | "description": "${this.name} id that needs to be deleted",
490 | "required": true,
491 | "type": "string"
492 | },
493 | {
494 | "in": "body",
495 | "name": "body",
496 | "description": "${this.name} object",
497 | "required": true,
498 | "schema": {
499 | "type": "object",
500 | "$ref": "#/definitions/${this.name}"
501 | }
502 | }
503 | ],
504 | "responses": {
505 | "200": {
506 | "description": "successful operation",
507 | "schema": {
508 | "type": "object",
509 | "properties": {
510 | "data":{
511 | "type":"object",
512 | "$ref": "#/definitions/${this.name}Response"
513 | }
514 | }
515 | }
516 | },
517 | "400": {
518 | "description": "Invalid status value",
519 | "schema": {
520 | "$ref": "#/definitions/InvalidResponse"
521 | }
522 | },
523 | "404":{
524 | "description": "Couldn't Find",
525 | "schema": {
526 | "$ref": "#/definitions/InvalidResponse"
527 | }
528 | }
529 | }
530 | },
531 | "delete" : {
532 | "summary" : "Delete the ${this.name}",
533 | "description": "Delete the ${this.name}",
534 | "produces": ["application/json"],
535 | "tags":["${this.name}"],
536 | "parameters": [
537 | {
538 | "name": "id",
539 | "in": "path",
540 | "description": "${this.name} id that needs to be deleted",
541 | "required": true,
542 | "type": "string"
543 | }
544 | ],
545 | "responses": {
546 | "200": {
547 | "description": "successful operation",
548 | "schema": {
549 | "type": "obj",
550 | "$ref": "#/definitions/DeleteResponse"
551 | }
552 | },
553 | "400": {
554 | "description": "Invalid id",
555 | "schema": {
556 | "$ref": "#/definitions/InvalidResponse"
557 | }
558 | },
559 | "404":{
560 | "description": "Couldn't Find",
561 | "schema": {
562 | "$ref": "#/definitions/InvalidResponse"
563 | }
564 | }
565 | }
566 | }
567 | },\n`;
568 | this.apiDocDefinitionString = `"${this.name}Response": {
569 | "type": "object",
570 | "properties": {
571 | "id": {
572 | "type": "integer"
573 | },
574 | ${this.attributes.staticField.map((staticField) => {
575 | return `"${staticField.name}":{\n "type":"${
576 | this.mapPrismaToSwagger[staticField.type]
577 | }"\n} \n`;
578 | })},
579 | ${this.attributes.relationalField.map((relationalField) => {
580 | return `"${relationalField.connection}":{\n "type":"integer"\n} \n`;
581 | })}
582 | }
583 | },
584 | "${this.name}": {
585 | "type": "object",
586 | "properties": {
587 | ${this.attributes.staticField.map((staticField) => {
588 | return `"${staticField.name}":{\n "type":"${
589 | this.mapPrismaToSwagger[staticField.type]
590 | }"\n} \n`;
591 | })},
592 | ${this.attributes.relationalField.map((relationalField) => {
593 | return `"${relationalField.connection}":{\n "type":"integer"\n} \n`;
594 | })}
595 | }
596 | },`;
597 | }
598 | }
599 |
600 | // CONVERT STRING TO ARRAYS OF STRINGS AND FINALLY USE THIS ARRAY
601 |
--------------------------------------------------------------------------------
/src/utils/SeedClass.ts:
--------------------------------------------------------------------------------
1 | import { Model } from "./ModelClass";
2 | import { Enum } from "./EnumClass";
3 | import { convertToUpperCamelCase } from "./common";
4 | import * as fse from "fs-extra";
5 | import path from "path";
6 |
7 | export type adjListType = {
8 | [key: string]: Array;
9 | };
10 |
11 | export class SeedDataGeneration {
12 | models: Array;
13 | enums: Array;
14 | adjacencyList: adjListType = {};
15 | visitedModels: Set;
16 | topologicallySortedModels: Array = [];
17 | seedFilePath = path.join(process.cwd(), "/app/prisma/seed.ts");
18 | seedFileString = "";
19 |
20 | constructor(models: Array, enums: Array) {
21 | this.models = models;
22 | this.enums = enums;
23 |
24 | // Creating adjacency list
25 | for (const independentModel of this.models) {
26 | for (const dependentModel of independentModel.attributes
27 | .relationalField) {
28 | if (dependentModel.connection in this.adjacencyList) {
29 | this.adjacencyList[dependentModel.connection].push(
30 | independentModel.name
31 | );
32 | } else {
33 | this.adjacencyList[dependentModel.connection] = [
34 | independentModel.name,
35 | ];
36 | }
37 | }
38 | }
39 |
40 | this.visitedModels = new Set();
41 | this.topologicalSort();
42 | this.generateSeedFileString();
43 | }
44 |
45 | topologicalSort() {
46 | for (const model of this.models) {
47 | if (!this.visitedModels.has(model.name)) {
48 | this.topologicalSortDFS(model.name);
49 | }
50 | }
51 | this.topologicallySortedModels.reverse();
52 | }
53 |
54 | topologicalSortDFS(modelName: string) {
55 | this.visitedModels.add(modelName);
56 |
57 | if (this.adjacencyList[modelName] !== undefined) {
58 | for (const nbr of this.adjacencyList[modelName]) {
59 | if (!this.visitedModels.has(nbr)) this.topologicalSortDFS(nbr);
60 | }
61 | }
62 |
63 | // Finding the model from modelName to push into the topological sorting stack
64 | const modelToPush = this.models.find(
65 | (model) => model.name === modelName
66 | ) as Model;
67 | this.topologicallySortedModels.push(modelToPush);
68 | }
69 |
70 | generateSeedFileString() {
71 | this.seedFileString = `import {
72 | Prisma,
73 | PrismaClient,
74 | ${this.models.map((model) => model.name).join(",")}
75 | } from "@prisma/client";
76 | import { faker } from "@faker-js/faker";
77 |
78 | const getRandomListElement = (items) => {
79 | return items[Math.floor(Math.random() * items.length)];
80 | };
81 |
82 | ${this.models
83 | .map((model) => {
84 | return `const new${convertToUpperCamelCase(
85 | model.name
86 | )} = (${model.attributes.relationalField
87 | .map((field) => `${field.connection}: ${field.connection}`)
88 | .join(",")}): Prisma.${model.name}CreateInput => {
89 | return {
90 | ${[
91 | ...model.attributes.staticField.map((field) => {
92 | // Get faker module and method for native Prisma types
93 | const faker = prismaFakerMapping[field.type];
94 |
95 | if (faker !== undefined) {
96 | let { module, method } = faker;
97 | // Override module and method as per user input
98 | if (field?.faker?.module && field?.faker?.method) {
99 | module = field.faker.module;
100 | method = field.faker.method;
101 | }
102 | return `${field.name}: faker.${module}.${method}()`;
103 | }
104 |
105 | // Handling enums
106 | const userEnum = this.enums.find(
107 | (e) => e.name === field.type
108 | );
109 | if (userEnum !== undefined) {
110 | return `${
111 | field.name
112 | }: getRandomListElement([${userEnum.fields.map(
113 | (enumName) => `"${enumName}"`
114 | )}])`;
115 | }
116 |
117 | throw new Error(
118 | `Type of field: ${field.name} is neither a Prisma data type nor an enum`
119 | );
120 | }),
121 | ...model.attributes.relationalField.map((field) => {
122 | return `${field.connection}: { connect: { id: ${field.connection}.id } }`;
123 | }),
124 | ].join(",\n")}
125 | };
126 | };`;
127 | })
128 | .join("\n\n")}
129 |
130 | const prisma = new PrismaClient();
131 |
132 | export const seed = async () => {
133 | const ROWS = 10;
134 |
135 | ${this.models
136 | .map((model) => `const ${model.name}s: Array<${model.name}> = [];`)
137 | .join("\n")}
138 |
139 | Array.from({ length: ROWS }).forEach(async () => {
140 | ${this.topologicallySortedModels
141 | .map((model) => {
142 | return `const ${model.name} = await prisma.${
143 | model.name
144 | }.create({ data: new${convertToUpperCamelCase(
145 | model.name
146 | )}(${model.attributes.relationalField
147 | .map((field) => `getRandomListElement(${field.connection}s)`)
148 | .join(",")}) });
149 | ${model.name}s.push(${model.name});\n`;
150 | })
151 | .join("\n")}
152 | });
153 |
154 | await prisma.$disconnect();
155 | };
156 | seed();
157 | export default seed;
158 | `;
159 |
160 | fse.outputFileSync(this.seedFilePath, this.seedFileString);
161 | }
162 | }
163 |
164 | export type PrismaFakerMapping = {
165 | [key: string]: {
166 | module: string;
167 | method: string;
168 | };
169 | };
170 |
171 | export const prismaFakerMapping: PrismaFakerMapping = {
172 | String: {
173 | module: "lorem",
174 | method: "word",
175 | },
176 | Boolean: {
177 | module: "datatype",
178 | method: "boolean",
179 | },
180 | Int: {
181 | module: "datatype",
182 | method: "number",
183 | },
184 | BigInt: {
185 | module: "datatype",
186 | method: "bigInt",
187 | },
188 | Float: {
189 | module: "datatype",
190 | method: "float",
191 | },
192 | Decimal: {
193 | module: "datatype",
194 | method: "float",
195 | },
196 | DateTime: {
197 | module: "datatype",
198 | method: "datetime",
199 | },
200 | Json: {
201 | module: "datatype",
202 | method: "json",
203 | },
204 | };
205 |
--------------------------------------------------------------------------------
/src/utils/common.ts:
--------------------------------------------------------------------------------
1 | export const convertToUpperCamelCase = (word: string) =>
2 | word.charAt(0).toUpperCase() + word.slice(1);
3 |
--------------------------------------------------------------------------------
/src/utils/getFields.ts:
--------------------------------------------------------------------------------
1 | import { RelationalField, StaticField, type } from "./ModelClass";
2 |
3 | export function getStaticFields(dataModel: any): Array {
4 | const staticFields: Array = [];
5 |
6 | for (const staticField of dataModel.attributes.StaticFields) {
7 | const newStaticField: StaticField = {
8 | name: staticField.name,
9 | type: staticField.type,
10 | isUnique: staticField.isUnique,
11 | toBeHashed: staticField.toBeHashed,
12 | faker: staticField.faker,
13 | defaultValue: staticField.defaultValue,
14 | };
15 | staticFields.push(newStaticField);
16 | }
17 | return staticFields;
18 | }
19 |
20 | export function getRelationalFields(dataModel: any): Array {
21 | const relationalFields: Array = [];
22 |
23 | for (const relationalField of dataModel.attributes.RelationalFields) {
24 | const newRelationalField: RelationalField = {
25 | name: relationalField.name,
26 | connection: relationalField.connection,
27 | foreignKey: relationalField.foriegnKeyName,
28 | type: relationalField.type as unknown as type,
29 | };
30 | relationalFields.push(newRelationalField);
31 | }
32 | return relationalFields;
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/schema.ts:
--------------------------------------------------------------------------------
1 | export type JoiMapping = {
2 | [index: string]: string;
3 | };
4 | export const joiMapping: JoiMapping = {
5 | String: `Joi.string().allow("")`,
6 | Int: "Joi.number().integer().strict()",
7 | Json: "Joi.any()",
8 | Boolean: "Joi.bool()",
9 | Float: "Joi.number()",
10 | DateTime: "Joi.string()",
11 | BigInt: "Joi.number()",
12 | Decimal: "Joi.number()",
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/validateInput.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 |
3 | const staticFieldSchema = Joi.object().keys({
4 | name: Joi.string().required(),
5 | type: Joi.string().required(),
6 | isUnique: Joi.boolean().optional(),
7 | toBeHashed: Joi.boolean().optional(),
8 | faker: Joi.any(),
9 | defaultValue: Joi.string().optional(),
10 | });
11 |
12 | const relationalFieldSchema = Joi.object().keys({
13 | name: Joi.string().required(),
14 | connection: Joi.string().required(),
15 | foriegnKeyName: Joi.string().required(),
16 | targetKeyName: Joi.string().optional(),
17 | type: Joi.string().valid("ONETOMANY", "ONETOONE").required(),
18 | });
19 |
20 | const attributeSchema = Joi.object().keys({
21 | StaticFields: Joi.array().items(staticFieldSchema).required(),
22 | RelationalFields: Joi.array().items(relationalFieldSchema).required(),
23 | });
24 |
25 | const model = Joi.object().keys({
26 | name: Joi.string().required(),
27 | attributes: attributeSchema,
28 | softDelete: Joi.boolean().optional().default(true),
29 | });
30 |
31 | const schema = Joi.object().keys({
32 | Models: Joi.array().items(model).required(),
33 | Authentication: Joi.any().optional(),
34 | Enums: Joi.array().optional(),
35 | default: Joi.any(),
36 | });
37 |
38 | const isJsonString = (str: string) => {
39 | try {
40 | JSON.parse(str);
41 | } catch (e) {
42 | return false;
43 | }
44 | return true;
45 | };
46 |
47 | export const validateInput = (inp: JSON) => {
48 | if (isJsonString(JSON.stringify(inp))) {
49 | const { error } = schema.validate(inp);
50 | if (!error) return { message: "All good" };
51 | const msg = error.details[0].message;
52 | return { error: msg };
53 | }
54 | return { error: "not a valid JSON" };
55 | };
56 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "commonjs" /* Specify what module code is generated. */,
28 | // "rootDir": "./", /* Specify the root folder within your source files. */
29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | "baseUrl": "./src/" /* Specify the base directory to resolve non-relative module names. */,
31 | "paths": {
32 | "~/*": ["./*"]
33 | } /* Specify a set of entries that re-map imports to additional lookup locations. */,
34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
35 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
38 | "resolveJsonModule": true /* Enable importing .json files */,
39 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
45 |
46 | /* Emit */
47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
52 | "outDir": "./dist/" /* Specify an output folder for all emitted files. */,
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
77 |
78 | /* Type Checking */
79 | "strict": true /* Enable all strict type-checking options. */,
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
81 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
86 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | },
103 | "exclude": ["./src/assets"],
104 | "include": ["./src"]
105 | }
106 |
--------------------------------------------------------------------------------