├── .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 | BFS 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 | Step 1: Create a new folder for your project 208 | 209 |

Step 2: Create your schema as a JSON file

210 | Step 2: Create your schema as a JSON file 211 | 212 |

Step 3: Install the crudify-dtu NPM package

213 | Step 3: Install the crudify-dtu NPM package 214 | 215 |

Step 4: CRUDify your ER Diagram using npx crudify-dtu “schema.json” command

216 | Step 4: CRUDify your ER Diagram using npx crudify-dtu “schema.json” command 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 | Equivalent schema created in Prisma ORM 222 | 223 |

You can see app/src/routes/ contains the APIs for blog, review and user models

224 | APIs for blog, review and user models 225 | 226 |

Step 5: cd into app directory and follow the instructions shown below for setup

227 | Instructions shown below for setup 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 | --------------------------------------------------------------------------------