├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.md └── workflows │ └── node-ci.yml ├── .gitignore ├── .releaserc.json ├── CONTRIBUTING.MD ├── LICENSE ├── README.MD ├── package-lock.json ├── package.json ├── src ├── cli │ ├── index.ts │ ├── interactive.ts │ ├── lib │ │ ├── git.ts │ │ └── toolchain │ │ │ ├── extensions.ts │ │ │ ├── generation.ts │ │ │ └── middleware.ts │ ├── logger.ts │ ├── messages.ts │ ├── parser.ts │ └── utils.ts ├── index.ts ├── lib │ ├── config.ts │ ├── functions.ts │ ├── generated.ts │ └── utils.ts ├── schema-creator │ ├── creator.ts │ ├── enum.ts │ ├── index.ts │ └── model.ts ├── toolchain │ ├── extensions │ │ └── index.ts │ ├── index.ts │ └── middleware │ │ └── index.ts └── version.ts ├── tools ├── copy-distribution-l.js ├── copy-distribution.js └── set-env.js └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report to help us improve Prisma 3 | labels: ['kind/bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for helping us improve Prisma Util! 9 | - type: textarea 10 | attributes: 11 | label: Bug description 12 | description: A clear and concise description of what the bug is. 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: How to reproduce 18 | description: Steps to reproduce the behavior 19 | value: | 20 | 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Expected behavior 31 | description: A clear and concise description of what you expected to happen. 32 | - type: textarea 33 | attributes: 34 | label: Prisma information 35 | description: Your Prisma schemas, command you tried to run, generated schema, ... 36 | value: | 37 | 38 | validations: 39 | required: true 40 | - type: textarea 41 | attributes: 42 | label: Environment & setup 43 | description: In which environment does the problem occur 44 | value: | 45 | - OS: 46 | - Database: 47 | - Node.js version: 48 | validations: 49 | required: true 50 | - type: textarea 51 | attributes: 52 | label: Prisma Version 53 | description: Run `prisma-util -v` to see your Prisma version and paste it between the ´´´ 54 | value: | 55 | ``` 56 | 57 | ``` 58 | validations: 59 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ## Problem 10 | 11 | 12 | 13 | ## Suggested solution 14 | 15 | 16 | 17 | ## Alternatives 18 | 19 | 20 | 21 | ## Additional context 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/node-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: '*' 11 | 12 | jobs: 13 | quality: 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 15.x] 20 | os: [ubuntu-latest] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: npm ci 30 | 31 | publish: 32 | runs-on: ubuntu-latest 33 | if: ${{ github.ref == 'refs/heads/main' }} 34 | needs: [quality] 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@v2 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | - run: npm ci 42 | - run: mkdir build 43 | - run: cp package.json build/package.json 44 | - run: npm run semantic-release 45 | env: 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | prisma-util-*.tgz 4 | # Keep environment variables out of version control 5 | .env 6 | *.prisma 7 | prisma-util.config.json 8 | prisma-util.config.mjs -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main", {"name": "beta", "prerelease": true}], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | [ 7 | "@semantic-release/exec", 8 | { 9 | "prepareCmd": "replace-json-property build/package.json version ${nextRelease.version}" 10 | } 11 | ], 12 | ["@semantic-release/npm", { 13 | "pkgRoot": "build" 14 | }], 15 | [ 16 | "@semantic-release/git", 17 | { 18 | "message": "Release <%= nextRelease.version %> [skip ci]", 19 | "assets": ["package.json"] 20 | } 21 | ], 22 | "@semantic-release/changelog" 23 | ], 24 | "repositoryFull": "https://github.com/DavidHancu/prisma-util.git" 25 | } -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Welcome! 2 | 3 | Thanks for trying to make Prisma Util better for everyone! On this page, you'll see how you can start working on the prisma-util CLI right away by creating a local copy of the code, editing it and then submitting the changes in a pull request. We review all pull requests accordingly, so make sure that your code is clean and readable. 4 | 5 | ## Contributing Code 6 | 7 | To start working on Prisma Util, you need to follow the steps below. Keep in mind that the general prerequisites are a requirement and can't be skipped. The repository is coded in `Typescript`, so make sure that you don't make changes in the `dist` directory, but rather the `src` one. 8 | 9 | ## General Prerequisites 10 | 11 | 1. Install Node.js `>=10` minimum, [latest LTS is recommended](https://nodejs.org/en/about/releases/) 12 | 13 | - Recommended: use [`nvm`](https://github.com/nvm-sh/nvm) for managing Node.js versions 14 | 15 | ## General Setup 16 | 17 | To set up and create your development environment, follow these steps: 18 | 19 | ```bash 20 | git clone https://github.com/DavidHancu/prisma-util.git 21 | cd prisma-util 22 | npm i 23 | ``` 24 | 25 | ## Building packages when you make changes 26 | 27 | We have provided a few scripts for you to run to make it easier when developing for Prisma Util. These can be invoked via NPM. 28 | 29 | To compile the code: 30 | ```sh 31 | npm run build 32 | ``` 33 | 34 | To compile the code and install the compiled version on your device: 35 | ```sh 36 | npm run local 37 | ``` 38 | 39 | ## Conventions 40 | 41 | ### Git Commit Messages 42 | 43 | We structure our messages like this: 44 | 45 | ``` 46 | : 47 | 48 | 49 | ``` 50 | 51 | Example 52 | 53 | ``` 54 | fix: base schema not including generator 55 | 56 | Closes #111 57 | ``` 58 | 59 | List of types: 60 | 61 | - feat: A new feature 62 | - fix: A bug fix 63 | - docs: Documentation only changes 64 | - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 65 | - refactor: A code change that neither fixes a bug nor adds a feature 66 | - perf: A code change that improves performance 67 | - test: Adding missing or correcting existing tests 68 | - chore: Changes to the build process or auxiliary tools and libraries such as documentation generation -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 David Hancu 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Prisma Util

4 | 5 | 6 | 7 |
8 |
9 | What is Prisma Util? 10 |   •   11 | How to use? 12 |   •   13 | Links 14 |   •   15 | Support 16 | 17 |
18 |
19 |
20 | 21 | ## What is Prisma Util? 22 | 23 | Prisma Util is an easy to use tool that merges multiple Prisma schema files, allows extending of models, resolves naming conflicts and provides easy access to Prisma commands and timing reports. It's mostly a plug-and-play replacement, with an easy configuration file. 24 | 25 | ## How to Use? 26 | 27 | npx prisma-util [options] 28 | 29 | The Prisma Util is built on top of Prisma, and as such all arguments and commands are the same as the ones from the [official documentation](https://www.prisma.io/docs/reference/api-reference/command-reference). The only additional parameter available is `--config` and it allows you to change the path of the config file (default: `prisma-util.config.mjs`). 30 | 31 | ## Links 32 | 33 | Check out our API Documentation - [API Documentation](https://prisma-util.gitbook.io/prisma-util/api-documentation) 34 | 35 | Get Started with Prisma Util - [Getting Started](https://prisma-util.gitbook.io/prisma-util/guides/getting-started) 36 | 37 | ## Support 38 | 39 | ### Create a bug report for Prisma Util 40 | 41 | If you see an error with Prisma Util, please create a bug report [here](https://github.com/DavidHancu/prisma-util/issues/new?assignees=&labels=&template=bug_report.md&title=). 42 | 43 | ### Submit a feature request 44 | 45 | If you want to see a new feature added to Prisma Util, please create an issue [here](https://github.com/DavidHancu/prisma-util/issues/new?assignees=&labels=&template=feature_request.md&title=). 46 | 47 | ### Contributing 48 | 49 | Refer to our [contribution guidelines](https://github.com/DavidHancu/prisma-util/blob/main/CONTRIBUTING.MD) for information on how to contribute. 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-util", 3 | "version": "0.0.0-development", 4 | "description": "Prisma Util is an easy to use tool that merges multiple Prisma schema files, allows extending of models, resolves naming conflicts both manually and automatically and provides easy access to Prisma commands and timing reports. It's mostly a plug-and-play replacement, with an easy confirguration file.", 5 | "main": "index.js", 6 | "bin": { 7 | "prisma-util": "cli/index.js" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "classify": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > ../src/version.ts", 12 | "manifest": "node ../tools/copy-distribution.js", 13 | "cleardistribution": "npx -y rimraf build", 14 | "prebuild": "npm run classify", 15 | "postbuild": "npm run manifest", 16 | "packer": "node tools/set-env.js && cd build && npm pack --pack-destination ../", 17 | "build": "tsc -p ../", 18 | "local": "npm run build && npm run packer", 19 | "run-l": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts && tsc -p . && node tools/copy-distribution-l.js && node tools/set-env.js && cd build && npm pack --pack-destination ../", 20 | "postversion": "npm run build", 21 | "semantic-release": "semantic-release" 22 | }, 23 | "keywords": [ 24 | "cli", 25 | "prisma", 26 | "prisma-cli", 27 | "prisma-util", 28 | "prisma-merge", 29 | "prisma-extend", 30 | "prisma-timings", 31 | "prisma-utility" 32 | ], 33 | "author": "DavidHancu", 34 | "license": "Apache-2.0", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/DavidHancu/prisma-util.git" 38 | }, 39 | "dependencies": { 40 | "@esbuild-kit/esm-loader": "^2.5.1", 41 | "@prisma/generator-helper": "^4.6.1", 42 | "axios": "^0.27.2", 43 | "chalk": "^5.0.1", 44 | "commander": "^9.4.0", 45 | "dotenv": "^16.0.3", 46 | "glob": "^8.0.3", 47 | "gradient-string": "^2.0.2", 48 | "inquirer": "^9.1.0", 49 | "json5": "^2.2.2", 50 | "ora": "^6.1.2", 51 | "pluralize": "^8.0.0", 52 | "resolve": "^1.22.1" 53 | }, 54 | "devDependencies": { 55 | "@semantic-release/changelog": "^6.0.2", 56 | "@semantic-release/commit-analyzer": "^9.0.2", 57 | "@semantic-release/exec": "^6.0.3", 58 | "@semantic-release/git": "^10.0.1", 59 | "@semantic-release/release-notes-generator": "^10.0.3", 60 | "@types/glob": "^8.0.0", 61 | "@types/gradient-string": "^1.1.2", 62 | "@types/inquirer": "^9.0.1", 63 | "@types/node": "^18.7.14", 64 | "@types/pluralize": "^0.0.29", 65 | "replace-json-property": "^1.8.0", 66 | "semantic-release": "^19.0.5", 67 | "typescript": "^4.8.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --no-warnings --experimental-specifier-resolution=node --loader @esbuild-kit/esm-loader 2 | 3 | import chalk from "chalk"; 4 | import * as commander from 'commander'; 5 | import { convertPathToLocal, createConfig, runPrismaCommand, usePrisma } from "./utils.js"; 6 | import MessageBuilder, { conflictTag, prismaCLITag, showIntro, successTag } from "./messages.js"; 7 | import PrismaParser from "./parser.js"; 8 | import { conflict, error, experimental, log, success, update, warn } from "./logger.js"; 9 | import ora from "ora"; 10 | import inquirer from "inquirer"; 11 | import { LIB_VERSION as current } from "../version.js"; 12 | import axios from "axios"; 13 | import * as fs from 'fs/promises'; 14 | import InteractiveMode from "./interactive.js"; 15 | 16 | // Requires node v14.7.0 17 | const program = new commander.Command(); 18 | 19 | // Initialize the parser to have it ready when the subcommand is used 20 | let parser: PrismaParser; 21 | let configPath: string = ""; 22 | let createdConfiguration: boolean = false; 23 | 24 | // Create the program instance and override the help menu 25 | program 26 | .name('prisma-util') 27 | .description('Prisma Util is an easy tool that helps with merging schema files and running utility commands.') 28 | .configureHelp({ 29 | formatHelp(cmd, helper) { 30 | showIntro(); 31 | return ""; 32 | } 33 | }) 34 | .configureOutput({ 35 | writeErr: (str) => { }, 36 | outputError: (str, write) => { } 37 | }) 38 | // Add sub command hook for creating the config file and reading from it 39 | .hook('preSubcommand', async (command, actionCommand) => { 40 | process.stdout.write(String.fromCharCode(27) + ']0;' + "Prisma Util" + String.fromCharCode(7)); 41 | 42 | // Check version before continuing. 43 | try { 44 | const latest = (await axios.get("https://registry.npmjs.com/prisma-util")).data["dist-tags"].latest; 45 | const [major, minor, patch] = latest.split(".").map((num: string) => Number.parseInt(num)); 46 | const [majorCurrent, minorCurrent, patchCurrent] = current.split(".").map((num: string) => Number.parseInt(num)); 47 | 48 | const development = process.env.ENV == "dev" || process.env.ENV == "beta"; 49 | if(!development && (major > majorCurrent || minor > minorCurrent || patch > patchCurrent)) { 50 | update(`There's an update available for Prisma Util! (current: v${current}, latest: v${latest})\n`, "\n"); 51 | } 52 | 53 | if(process.env.ENV == "beta") 54 | { 55 | new MessageBuilder() 56 | .withHeader() 57 | .withTitle(chalk.gray(`Thank your for beta testing ${chalk.blue("Prisma Util")}!\n Please provide any feedback you may have, as it helps out a ton!`)) 58 | .withNewLine() 59 | .withSection("Beta Information: ", 60 | [`${chalk.white("Getting Started")} ${chalk.gray("https://prisma-util.gitbook.io/beta/getting-started")}`]) 61 | .show(); 62 | } 63 | } catch (err) { 64 | error("An error has occured while trying to check the CLI version.\n", "\n"); 65 | } 66 | 67 | const { config, H, previewFeature } = actionCommand.optsWithGlobals(); 68 | configPath = config; 69 | 70 | if(actionCommand.parent && actionCommand.parent.args.length > 0) 71 | { 72 | const ignoreConfig: string[] = []; 73 | 74 | if(!ignoreConfig.includes(actionCommand.parent.args[0])) 75 | { 76 | const {configData, created} = await createConfig(config); 77 | createdConfiguration = created; 78 | // Don't load anything yet until we're sure that we need it 79 | parser = new PrismaParser(configData, config); 80 | await parser.loadEnvironment(); 81 | } 82 | } 83 | }) 84 | .hook("postAction", async (command, actionCommand) => { 85 | if(parser && parser.loaded) 86 | { 87 | await parser.toolchain(); 88 | } 89 | }) 90 | 91 | // Create configuration file 92 | program 93 | .command("prepare") 94 | .action(async (options) => { 95 | if(createdConfiguration) 96 | { 97 | new MessageBuilder() 98 | .withHeader() 99 | .withTitle(chalk.gray(`Welcome to ${chalk.blue("Prisma Util")}!\n The configuration file has been generated in ${chalk.blue("./prisma-util/config.mjs")}.`)) 100 | .withNewLine() 101 | .withSection("If you are new to Prisma Util, we recommend the following guides: ", [`${chalk.white("Getting Started")} ${chalk.gray("https://prisma-util.gitbook.io/main/guides/getting-started")}`]) 102 | .show(); 103 | 104 | return; 105 | } 106 | }); 107 | 108 | program 109 | .command("interactive") 110 | .option("--tutorial, --guide ") 111 | .action(async (options) => { 112 | new InteractiveMode(options.guide ? options.guide : undefined); 113 | }); 114 | 115 | // Configure Prisma Util 116 | const configure = program 117 | .command("configure") 118 | .action(async (options) => { 119 | if (options.H) { 120 | new MessageBuilder() 121 | .withHeader() 122 | .withTitle(chalk.gray("Configure your Prisma Util instance")) 123 | .withNewLine() 124 | .withSection("Usage", [`${chalk.gray("$")} prisma-util configure `]) 125 | .withSection("Options", [`${chalk.gray("root ")} Change the Prisma Util Root`, 126 | `${chalk.gray("config ")} Change the configuration file name`]) 127 | .show(); 128 | return; 129 | } 130 | }); 131 | createSubCommand(configure, "root ") 132 | .action(async (value, options) => { 133 | options = configure.optsWithGlobals(); 134 | options.args = { value }; 135 | 136 | if(options.args.value) 137 | { 138 | const packConfig = JSON.parse(await fs.readFile(convertPathToLocal("./package.json"), "utf8")); 139 | packConfig.prismaUtil = options.args.value; 140 | await fs.writeFile(convertPathToLocal("package.json"), JSON.stringify(packConfig, null, 2)); 141 | 142 | success(`Changed the ${chalk.bold("root folder")} to ${chalk.bold(value)}.`, "\n"); 143 | } 144 | }); 145 | createSubCommand(configure, "config ") 146 | .action(async (value, options) => { 147 | options = configure.optsWithGlobals(); 148 | options.args = { value }; 149 | 150 | if(options.args.value) 151 | { 152 | const packConfig = JSON.parse(await fs.readFile(convertPathToLocal("./package.json"), "utf8")); 153 | packConfig.prismaUtilConfig = options.args.value; 154 | await fs.writeFile(convertPathToLocal("package.json"), JSON.stringify(packConfig, null, 2)); 155 | 156 | success(`Changed the ${chalk.bold("configuration name")} to ${chalk.bold(value)}.`, "\n"); 157 | } 158 | }); 159 | 160 | // Match Prisma's version command 161 | program 162 | .command("version") 163 | .alias("v") 164 | .description("The version command outputs information about your current prisma version, platform, and engine binaries.") 165 | .option("--json", "Outputs version information in JSON format.") 166 | .action(async (options) => { 167 | // Help menu 168 | if (options.H) { 169 | new MessageBuilder() 170 | .withHeader() 171 | .withTitle(chalk.gray("Print current version of Prisma components")) 172 | .withNewLine() 173 | .withSection("Usage", [`${chalk.gray("$")} prisma-util -v [options]`, `${chalk.gray("$")} prisma-util version [options]`]) 174 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`, `${chalk.gray("--json")} Output JSON`]) 175 | .show(); 176 | return; 177 | } 178 | await runPrismaCommand(`version${options.json ? " --json" : ""}${options.previewFeature ? " --preview-feature" : ""}`); 179 | }); 180 | 181 | // Default command for when no subcommands have been added 182 | program 183 | .command("help", { isDefault: true }) 184 | .description("Help menu for Prisma Util.") 185 | .action(async () => { 186 | showIntro() 187 | }); 188 | 189 | function verifyDatasourceProvider(provider: string, dummyPrevious: string) { 190 | const allowed = ['sqlite', 'postgresql', 'mysql', 'sqlserver', 'mongodb', 'cockroachdb']; 191 | if (!allowed.includes(provider)) { 192 | error(`Provider ${chalk.bold(`${provider}`)} is invalid or not supported. Try again with "postgresql", "mysql", "sqlite", "sqlserver", "mongodb" or "cockroachdb".`); 193 | throw new commander.InvalidArgumentError(`Provider ${chalk.bold(`${provider}`)} is invalid or not supported. Try again with "postgresql", "mysql", "sqlite", "sqlserver", "mongodb" or "cockroachdb".`) 194 | } 195 | return provider; 196 | } 197 | 198 | // Match Prisma's init command 199 | program 200 | .command("init") 201 | .description("The init command does not interpret any existing files. Instead, it creates a prisma directory containing a bare-bones schema.prisma file within your current directory.") 202 | .option("--datasource-provider [provider]", "Specifies the default value for the provider field in the datasource block.", verifyDatasourceProvider, "postgresql") 203 | .option("--url [url]", "Define a custom datasource url.", "null") 204 | .action(async (options) => { 205 | // Help menu 206 | if (options.H) { 207 | new MessageBuilder() 208 | .withHeader() 209 | .withTitle(chalk.gray("Set up a new Prisma project")) 210 | .withNewLine() 211 | .withSection("Usage", [`${chalk.gray("$")} prisma-util init [options]`]) 212 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`, `${chalk.gray("--datasource-provider")} Define the datasource provider to use: PostgreSQL, MySQL, SQLite, SQL Server or MongoDB`, `${chalk.gray("--url")} Define a custom datasource url`]) 213 | .withSection("Examples", 214 | [chalk.gray("Set up a new Prisma project with PostgreSQL (default)"), `${chalk.gray("$")} prisma-util init`, "", 215 | chalk.gray("Set up a new Prisma project and specify MySQL as the datasource provider to use"), `${chalk.gray("$")} prisma-util init --datasource-provider mysql`, "", 216 | chalk.gray("Set up a new Prisma project and specify the url that will be used"), `${chalk.gray("$")} prisma-util init --url mysql://user:password@localhost:3306/mydb` 217 | ]) 218 | .show(); 219 | return; 220 | } 221 | 222 | if (typeof options.datasourceProvider == "boolean") { 223 | error(`Provider ${chalk.bold(`${options.datasourceProvider}`)} is invalid or not supported. Try again with "postgresql", "mysql", "sqlite", "sqlserver", "mongodb" or "cockroachdb".`); 224 | return; 225 | } 226 | 227 | if (typeof options.url == "boolean" || options.url == "null") { 228 | delete options["url"]; 229 | } 230 | 231 | await runPrismaCommand(`init${options.url ? ` --url ${options.url}` : ""}${options.datasourceProvider ? ` --datasource-provider ${options.datasourceProvider}` : ""}${options.previewFeature ? " --preview-feature" : ""}`); 232 | }); 233 | 234 | // Match Prisma's generate command 235 | program 236 | .command("generate") 237 | .description("The generate command generates assets like Prisma Client based on the generator and data model blocks defined in your prisma/schema.prisma file.") 238 | .option("--data-proxy [dataProxy]", "Define a custom datasource url.", "null") 239 | .action(async (options) => { 240 | // Help menu 241 | if (options.H) { 242 | new MessageBuilder() 243 | .withHeader() 244 | .withTitle(chalk.gray("Generate artifacts (e.g. Prisma Client)")) 245 | .withNewLine() 246 | .withSection("Usage", [`${chalk.gray("$")} prisma-util generate [options]`]) 247 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`, `${chalk.gray("--data-proxy")} Enable the Data Proxy in the Prisma Client`]) 248 | .show(); 249 | return; 250 | } 251 | 252 | if (typeof options.dataProxy == "boolean" || options.dataProxy == "null") { 253 | delete options["dataProxy"]; 254 | } 255 | 256 | parser = await parser.load(); 257 | 258 | await fixConflicts(); 259 | 260 | console.log(); 261 | const spinner = ora({ 262 | text: `${chalk.gray("Generating merged schema...")}`, 263 | prefixText: prismaCLITag 264 | }).start(); 265 | await parser.writeSchema(); 266 | spinner.stopAndPersist({ 267 | text: `${chalk.gray("Merged schema generated successfully.")}`, 268 | prefixText: '', 269 | symbol: successTag 270 | }); 271 | 272 | await runPrismaCommand(`generate --schema ./node_modules/.bin/generated-schema.prisma${options.dataProxy ? ` --data-proxy ${options.dataProxy}` : ""}${options.previewFeature ? " --preview-feature" : ""}`); 273 | await parser.generate(); 274 | }); 275 | 276 | function commaSeparatedList(value: string) { 277 | return value.split(','); 278 | } 279 | // Match Prisma's format command 280 | program 281 | .command("format") 282 | .description("Format a Prisma schema.") 283 | .option("--schema [schemas]", "The schemas to format", "") 284 | .action(async (options) => { 285 | // Help menu 286 | if (options.H) { 287 | new MessageBuilder() 288 | .withHeader() 289 | .withTitle(chalk.gray("Format a Prisma schema.")) 290 | .withNewLine() 291 | .withSection("Usage", [`${chalk.gray("$")} prisma-util format [options]`]) 292 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`]) 293 | .show(); 294 | return; 295 | } 296 | 297 | if(options.schema && options.schema.trim() != "") 298 | { 299 | const schemas = commaSeparatedList(options.schema.trim()); 300 | for(const schema of schemas) 301 | { 302 | await runPrismaCommand(`format --schema ${schema}${options.previewFeature ? " --preview-feature" : ""}`); 303 | } 304 | } else 305 | { 306 | parser = await parser.load(); 307 | 308 | await fixConflicts(); 309 | 310 | console.log(); 311 | const spinner = ora({ 312 | text: `${chalk.gray("Generating merged schema...")}`, 313 | prefixText: prismaCLITag 314 | }).start(); 315 | await parser.writeSchema(); 316 | spinner.stopAndPersist({ 317 | text: `${chalk.gray("Merged schema generated successfully.")}`, 318 | prefixText: '', 319 | symbol: successTag 320 | }); 321 | await runPrismaCommand(`format --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 322 | } 323 | }); 324 | 325 | // Match Prisma's validate command 326 | program 327 | .command("validate") 328 | .description("Validate a Prisma schema.") 329 | .action(async (options) => { 330 | // Help menu 331 | if (options.H) { 332 | new MessageBuilder() 333 | .withHeader() 334 | .withTitle(chalk.gray("Validate a Prisma schema.")) 335 | .withNewLine() 336 | .withSection("Usage", [`${chalk.gray("$")} prisma-util validate [options]`]) 337 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`]) 338 | .show(); 339 | return; 340 | } 341 | 342 | await runPrismaCommand(`validate --schema ./${parser.config.baseSchema}${options.previewFeature ? " --preview-feature" : ""}`); 343 | }); 344 | 345 | // Match Prisma's db command 346 | const db = program 347 | .command("db") 348 | .description("Manage your database schema and lifecycle during development.") 349 | .action((options) => { 350 | new MessageBuilder() 351 | .withHeader() 352 | .withTitle(chalk.gray("Manage your database schema and lifecycle during development.")) 353 | .withNewLine() 354 | .withSection("Usage", [`${chalk.gray("$")} prisma-util db [command] [options]`]) 355 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`]) 356 | .withSection("Commands", 357 | [` ${chalk.gray("pull")} Pull the state from the database to the Prisma schema using introspection`, 358 | ` ${chalk.gray("push")} Push the state from Prisma schema to the database during prototyping`, 359 | ` ${chalk.gray("seed")} Seed your database`, 360 | ]) 361 | .show(); 362 | }); 363 | 364 | function compositeTypeDepthParser(value: string, dummyPrevious: string) { 365 | const parsedValue = parseInt(value, 10); 366 | if (isNaN(parsedValue)) { 367 | error(`Argument ${chalk.bold(`${value}`)} is not a number.`); 368 | throw new commander.InvalidArgumentError(`Argument ${chalk.bold(`${value}`)} is not a number.`); 369 | } 370 | return value; 371 | } 372 | createSubCommand(db, "pull") 373 | .option("--force", "Ignore current Prisma schema file") 374 | .option("--print", "Print the introspected Prisma schema to stdout") 375 | .option("--composite-type-depth [compositeTypeDepth]", "Specify the depth for introspecting composite types", compositeTypeDepthParser, "-1") 376 | .action(async (options, command) => { 377 | options = command.optsWithGlobals(); 378 | options.compositeTypeDepth = parseInt(options.compositeTypeDepth, 10); 379 | 380 | if (options.H) { 381 | new MessageBuilder() 382 | .withHeader() 383 | .withTitle(chalk.gray("Pull the state from the database to the Prisma schema using introspection")) 384 | .withNewLine() 385 | .withSection("Usage", [`${chalk.gray("$")} prisma-util db pull [flags/options]`]) 386 | .withSection("Flags", [`${chalk.gray("-h, --help")} Display this help message`, ` ${chalk.gray("--force")} Ignore current Prisma schema file`, ` ${chalk.gray("--print")} Print the introspected Prisma schema to stdout`]) 387 | .withSection("Options", 388 | [`${chalk.gray("--composite-type-depth")} Specify the depth for introspecting composite types\n (e.g. Embedded Documents in MongoDB)\n Number, default is -1 for infinite depth, 0 = off`, 389 | ]) 390 | .withSection("Examples", 391 | [chalk.gray("Instead of saving the result to the filesystem, you can also print it to stdout"), `${chalk.gray("$")} prisma-util db pull --print`, "", 392 | chalk.gray("Overwrite the current schema with the introspected schema instead of enriching it"), `${chalk.gray("$")} prisma-util db pull --force`, "", 393 | chalk.gray("Set composite types introspection depth to 2 levels"), `${chalk.gray("$")} prisma-util db pull --composite-type-depth=2` 394 | ]) 395 | .show(); 396 | return; 397 | } 398 | 399 | await runPrismaCommand(`db pull${options.force ? " --force" : ""}${options.print ? " --print" : ""} --composite-type-depth ${options.compositeTypeDepth} --schema ./${parser.config.baseSchema}${options.previewFeature ? " --preview-feature" : ""}`); 400 | }); 401 | createSubCommand(db, "push") 402 | .option("--accept-data-loss", "Ignore data loss warnings") 403 | .option("--force-reset", "Force a reset of the database before push") 404 | .option("--skip-generate", "Skip triggering generators (e.g. Prisma Client)") 405 | .action(async (options, command) => { 406 | options = command.optsWithGlobals(); 407 | 408 | if (options.H) { 409 | new MessageBuilder() 410 | .withHeader() 411 | .withTitle(chalk.gray("Push the state from your Prisma schema to your database")) 412 | .withNewLine() 413 | .withSection("Usage", [`${chalk.gray("$")} prisma-util db push [options]`]) 414 | .withSection("Options", 415 | [` ${chalk.gray("-h, --help")} Display this help message`, 416 | ` ${chalk.gray("--accept-data-loss")} Ignore data loss warnings`, 417 | ` ${chalk.gray("--force-reset")} Force a reset of the database before push`, 418 | ` ${chalk.gray("--skip-generate")} Skip triggering generators (e.g. Prisma Client)`, 419 | ]) 420 | .withSection("Examples", 421 | [chalk.gray("Ignore data loss warnings"), `${chalk.gray("$")} prisma-util db push --accept-data-loss` 422 | ]) 423 | .show(); 424 | return; 425 | } 426 | 427 | parser = await parser.load(); 428 | 429 | await fixConflicts(); 430 | 431 | console.log(); 432 | const spinner = ora({ 433 | text: `${chalk.gray("Generating merged schema...")}`, 434 | prefixText: prismaCLITag 435 | }).start(); 436 | await parser.writeSchema(); 437 | spinner.stopAndPersist({ 438 | text: `${chalk.gray("Merged schema generated successfully.")}`, 439 | prefixText: '', 440 | symbol: successTag 441 | }); 442 | 443 | await runPrismaCommand(`db push${options.acceptDataLoss ? " --accept-data-loss" : ""}${options.forceReset ? " --force-reset" : ""}${options.skipGenerate ? " --skip-generate" : ""} --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 444 | if(!options.skipGenerate) 445 | await parser.generate(); 446 | }); 447 | createSubCommand(db, "seed") 448 | .description("Seed your database") 449 | .action(async (options, command) => { 450 | options = command.optsWithGlobals(); 451 | 452 | if (options.H) { 453 | new MessageBuilder() 454 | .withHeader() 455 | .withTitle(chalk.gray("Seed your database")) 456 | .withNewLine() 457 | .withSection("Usage", [`${chalk.gray("$")} prisma-util db seed [options]`]) 458 | .withSection("Options", 459 | [`${chalk.gray("-h, --help")} Display this help message`,]) 460 | .show(); 461 | return; 462 | } 463 | 464 | await runPrismaCommand(`db seed${options.previewFeature ? " --preview-feature" : ""}`); 465 | }); 466 | 467 | // Schema command for additional use-cases 468 | program 469 | .command("schema") 470 | .description("Generate schemas using Prisma Util without running additional commands") 471 | .option("--path [path]", "Path to save the file to.", "./node_modules/.bin/generated-schema.prisma") 472 | .action(async (options) => { 473 | if (options.H) { 474 | new MessageBuilder() 475 | .withHeader() 476 | .withTitle(chalk.gray("Generate schemas using Prisma Util without running additional commands")) 477 | .withNewLine() 478 | .withSection("Usage", [`${chalk.gray("$")} prisma-util schema [options]`]) 479 | .withSection("Options", 480 | [ 481 | `${chalk.gray("-h, --help")} Display this help message`, 482 | ` ${chalk.gray("--path")} Path to save the file to.` 483 | ]) 484 | .show(); 485 | return; 486 | } 487 | 488 | parser = await parser.load(); 489 | 490 | await fixConflicts(); 491 | 492 | console.log(); 493 | const spinner = ora({ 494 | text: `${chalk.gray("Generating merged schema...")}`, 495 | prefixText: prismaCLITag 496 | }).start(); 497 | await parser.writeSchema(options.path); 498 | spinner.stopAndPersist({ 499 | text: `${chalk.gray("Merged schema generated successfully.")}`, 500 | prefixText: '', 501 | symbol: successTag 502 | }); 503 | }); 504 | 505 | // Match Prisma's migrate command 506 | const migrate = program 507 | .command("migrate") 508 | .description("Update the database schema with migrations") 509 | .action((options) => { 510 | new MessageBuilder() 511 | .withHeader() 512 | .withTitle(chalk.gray("Update the database schema with migrations")) 513 | .withNewLine() 514 | .withSection("Usage", [`${chalk.gray("$")} prisma-util migrate [command] [options]`]) 515 | .withSection("Commands for development", 516 | [` ${chalk.gray("dev")} Create a migration from changes in Prisma schema, apply it to the database\n trigger generators (e.g. Prisma Client)`, 517 | `${chalk.gray("reset")} Reset your database and apply all migrations, all data will be lost` 518 | ]) 519 | .withSection("Commands for production/staging", 520 | [` ${chalk.gray("deploy")} Apply pending migrations to the database`, 521 | ` ${chalk.gray("status")} Check the status of your database migrations`, 522 | `${chalk.gray("resolve")} Resolve issues with database migrations, i.e. baseline, failed migration, hotfix` 523 | ]) 524 | .withSection("Commands for any stage", 525 | [`${chalk.gray("diff")} Compare the database schema from two arbitrary sources` 526 | ]) 527 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`]) 528 | .withSection("Examples", 529 | [chalk.gray("Create a migration from changes in Prisma schema, apply it to the database, trigger generators (e.g. Prisma Client)"), `${chalk.gray("$")} prisma-util migrate dev`, "", 530 | chalk.gray("Reset your database and apply all migrations"), `${chalk.gray("$")} prisma-util migrate reset`, "", 531 | chalk.gray("Apply pending migrations to the database in production/staging"), `${chalk.gray("$")} prisma-util migrate deploy`, "", 532 | chalk.gray("Check the status of migrations in the production/staging database"), `${chalk.gray("$")} prisma-util migrate status`, "", 533 | chalk.gray("Reset your database and apply all migrations"), `${chalk.gray("$")} prisma-util migrate reset`, "", 534 | chalk.gray("Compare the database schema from two databases and render the diff as a SQL script"), `${chalk.gray("$")} prisma-util migrate diff \\`, " --from-url \"$DATABASE_URL\" \\", " --to-url \"postgresql://login:password@localhost:5432/db\" \\", " --script", 535 | ]) 536 | .show(); 537 | }); 538 | createSubCommand(migrate, "dev") 539 | .option("-n, --name [name]", "Name the migration") 540 | .option("--create-only", "Create a new migration but do not apply it") 541 | .option("--skip-generate", "Skip triggering generators (e.g. Prisma Client)") 542 | .option("--skip-seed", "Skip triggering seed") 543 | .option("--force", "Bypass environment lock") 544 | .action(async (options, command) => { 545 | options = command.optsWithGlobals(); 546 | 547 | if (options.H) { 548 | new MessageBuilder() 549 | .withHeader() 550 | .withTitle(chalk.gray("Create a migration from changes in Prisma schema, apply it to the database, trigger generators (e.g. Prisma Client)")) 551 | .withNewLine() 552 | .withSection("Usage", [`${chalk.gray("$")} prisma-util migrate dev [options]`]) 553 | .withSection("Options", 554 | [` ${chalk.gray("-h, --help")} Display this help message`, 555 | ` ${chalk.gray("-n, --name")} Name the migration`, 556 | ` ${chalk.gray("--create-only")} Create a new migration but do not apply it\n The migration will be empty if there are no changes in Prisma schema`, 557 | `${chalk.gray("--skip-generate")} Skip triggering generators (e.g. Prisma Client)`, 558 | ` ${chalk.gray("--skip-seed")} Skip triggering seed` 559 | ]) 560 | .withSection("Examples", 561 | [chalk.gray("Create a migration from changes in Prisma schema, apply it to the database, trigger generators (e.g. Prisma Client)"), `${chalk.gray("$")} prisma-util migrate dev`, "", 562 | chalk.gray("Create a migration without applying it"), `${chalk.gray("$")} prisma-util migrate dev --create-only` 563 | ]) 564 | .show(); 565 | return; 566 | } 567 | 568 | parser = await parser.load(); 569 | 570 | if(parser.config.environmentLock && process.env.NODE_ENV == "production" && !options.force) 571 | { 572 | warn(`${chalk.bold("Environment Lock")}\nBecause you've enabled the ${chalk.bold("environmentLock")} optional feature in the configuration file, you can't run ${chalk.bold("migrate dev")} while ${chalk.bold("process.env.NODE_ENV")} is set to ${chalk.bold("production")}.\nTo bypass this lock, use the ${chalk.bold("--force")} flag or disable ${chalk.bold("environmentLock")}.`, "\n") 573 | process.exit(0); 574 | return; 575 | } 576 | 577 | await fixConflicts(); 578 | 579 | console.log(); 580 | const spinner = ora({ 581 | text: `${chalk.gray("Generating merged schema...")}`, 582 | prefixText: prismaCLITag 583 | }).start(); 584 | await parser.writeSchema(); 585 | spinner.stopAndPersist({ 586 | text: `${chalk.gray("Merged schema generated successfully.")}`, 587 | prefixText: '', 588 | symbol: successTag 589 | }); 590 | 591 | const modifiedMigration = await parser.migrate(`migrate dev${options.name ? ` -n ${options.name}` : ""}${options.skipSeed ? " --skip-seed" : ""}${options.skipGenerate ? " --skip-generate" : ""} --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 592 | if(modifiedMigration && options.createOnly) 593 | return; 594 | await runPrismaCommand(`migrate dev${options.name ? ` -n ${options.name}` : ""}${options.createOnly ? " --create-only" : ""}${options.skipSeed ? " --skip-seed" : ""}${options.skipGenerate ? " --skip-generate" : ""} --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 595 | 596 | if(await parser.fixMigrate()) 597 | { 598 | experimental("Retrying to run the command.", "\n"); 599 | await runPrismaCommand(`migrate deploy --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 600 | await runPrismaCommand(`generate --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 601 | } 602 | if(!options.skipGenerate) 603 | await parser.generate(); 604 | }); 605 | 606 | createSubCommand(migrate, "reset") 607 | .option("-f, --force", "Skip the confirmation prompt") 608 | .option("--skip-generate", "Skip triggering generators (e.g. Prisma Client)") 609 | .option("--skip-seed", "Skip triggering seed") 610 | .option("--reset-only", "Do not apply any migrations") 611 | .action(async (options, command) => { 612 | options = command.optsWithGlobals(); 613 | 614 | if (options.H) { 615 | new MessageBuilder() 616 | .withHeader() 617 | .withTitle(chalk.gray("Reset your database and apply all migrations, all data will be lost")) 618 | .withNewLine() 619 | .withSection("Usage", [`${chalk.gray("$")} prisma-util migrate reset [options]`]) 620 | .withSection("Options", 621 | [` ${chalk.gray("-h, --help")} Display this help message`, 622 | `${chalk.gray("--skip-generate")} Skip triggering generators (e.g. Prisma Client)`, 623 | ` ${chalk.gray("--skip-seed")} Skip triggering seed`, 624 | ` ${chalk.gray("-f, --force")} Skip the confirmation prompt` 625 | ]) 626 | .withSection("Examples", 627 | [chalk.gray("Reset your database and apply all migrations, all data will be lost"), `${chalk.gray("$")} prisma-util migrate reset`, "", 628 | chalk.gray("Use --force to skip the confirmation prompt"), `${chalk.gray("$")} prisma-util migrate reset --force` 629 | ]) 630 | .show(); 631 | return; 632 | } 633 | 634 | parser = await parser.load(); 635 | 636 | await fixConflicts(); 637 | 638 | console.log(); 639 | const spinner = ora({ 640 | text: `${chalk.gray("Generating merged schema...")}`, 641 | prefixText: prismaCLITag 642 | }).start(); 643 | await parser.writeSchema(); 644 | spinner.stopAndPersist({ 645 | text: `${chalk.gray("Merged schema generated successfully.")}`, 646 | prefixText: '', 647 | symbol: successTag 648 | }); 649 | 650 | if(options.resetOnly) 651 | { 652 | await parser.resetMigrations(); 653 | } 654 | await runPrismaCommand(`migrate reset${options.force ? ` --force` : ""}${options.skipSeed ? " --skip-seed" : ""}${options.skipGenerate ? " --skip-generate" : ""} --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 655 | if(!options.skipGenerate) 656 | await parser.generate(); 657 | }); 658 | createSubCommand(migrate, "deploy") 659 | .description("Apply pending migrations to update the database schema in production/staging") 660 | .action(async (options, command) => { 661 | options = command.optsWithGlobals(); 662 | 663 | if(options.H) 664 | { 665 | new MessageBuilder() 666 | .withHeader() 667 | .withTitle(chalk.gray("Apply pending migrations to update the database schema in production/staging")) 668 | .withNewLine() 669 | .withSection("Usage", [`${chalk.gray("$")} prisma-util migrate deploy [options]`]) 670 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`]) 671 | .show(); 672 | return; 673 | } 674 | 675 | parser = await parser.load(); 676 | 677 | await fixConflicts(); 678 | 679 | console.log(); 680 | const spinner = ora({ 681 | text: `${chalk.gray("Generating merged schema...")}`, 682 | prefixText: prismaCLITag 683 | }).start(); 684 | await parser.writeSchema(); 685 | spinner.stopAndPersist({ 686 | text: `${chalk.gray("Merged schema generated successfully.")}`, 687 | prefixText: '', 688 | symbol: successTag 689 | }); 690 | 691 | await runPrismaCommand(`migrate deploy --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 692 | }); 693 | createSubCommand(migrate, "resolve") 694 | .option("--applied [applied]", "Record a specific migration as applied") 695 | .option("--rolled-back [rolledBack]", "Record a specific migration as rolled back") 696 | .action(async (options, command) => { 697 | options = command.optsWithGlobals(); 698 | 699 | if(options.H) 700 | { 701 | new MessageBuilder() 702 | .withHeader() 703 | .withTitle(chalk.gray("Resolve issues with database migrations in deployment databases:")) 704 | .withTitle(chalk.gray("- recover from failed migrations")) 705 | .withTitle(chalk.gray("- baseline databases when starting to use Prisma Migrate on existing databases")) 706 | .withTitle(chalk.gray("- reconcile hotfixes done manually on databases with your migration history")) 707 | .withNewLine() 708 | .withTitle(chalk.gray(`Run ${chalk.blue("prisma-cli migrate status")} to identify if you need to use resolve.`)) 709 | .withNewLine() 710 | .withTitle(chalk.gray("Read more about resolving migration history issues: https://pris.ly/d/migrate-resolve")) 711 | .withNewLine() 712 | .withSection("Usage", [`${chalk.gray("$")} prisma-util migrate resolve [options]`]) 713 | .withSection("Options", 714 | [ 715 | ` ${chalk.gray("-h, --help")} Display this help message`, 716 | ` ${chalk.gray("--applied")} Record a specific migration as applied`, 717 | `${chalk.gray("--rolled-back")} Record a specific migration as rolled back` 718 | ]) 719 | .withSection("Examples", 720 | [ 721 | chalk.gray("Update migrations table, recording a specific migration as applied"), `${chalk.gray("$")} prisma-util migrate resolve --applied 20201231000000_add_users_table`, "", 722 | chalk.gray("Update migrations table, recording a specific migration as rolled back"), `${chalk.gray("$")} prisma-util migrate resolve --rolled-back 20201231000000_add_users_table` 723 | ]) 724 | .show(); 725 | return; 726 | } 727 | 728 | parser = await parser.load(); 729 | 730 | await fixConflicts(); 731 | 732 | console.log(); 733 | const spinner = ora({ 734 | text: `${chalk.gray("Generating merged schema...")}`, 735 | prefixText: prismaCLITag 736 | }).start(); 737 | await parser.writeSchema(); 738 | spinner.stopAndPersist({ 739 | text: `${chalk.gray("Merged schema generated successfully.")}`, 740 | prefixText: '', 741 | symbol: successTag 742 | }); 743 | 744 | await runPrismaCommand(`migrate resolve${options.applied ? ` --applied ${options.applied}` : ""}${options.rolledBack ? ` --rolled-back ${options.rolledBack}` : ""} --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 745 | }); 746 | createSubCommand(migrate, "status") 747 | .description("Check the status of your database migrations") 748 | .action(async (options, command) => { 749 | options = command.optsWithGlobals(); 750 | 751 | if(options.H) 752 | { 753 | new MessageBuilder() 754 | .withHeader() 755 | .withTitle(chalk.gray("Check the status of your database migrations")) 756 | .withNewLine() 757 | .withSection("Usage", [`${chalk.gray("$")} prisma-util migrate status [options]`]) 758 | .withSection("Options", [`${chalk.gray("-h, --help")} Display this help message`]) 759 | .show(); 760 | return; 761 | } 762 | 763 | parser = await parser.load(); 764 | 765 | await fixConflicts(); 766 | 767 | console.log(); 768 | const spinner = ora({ 769 | text: `${chalk.gray("Generating merged schema...")}`, 770 | prefixText: prismaCLITag 771 | }).start(); 772 | await parser.writeSchema(); 773 | spinner.stopAndPersist({ 774 | text: `${chalk.gray("Merged schema generated successfully.")}`, 775 | prefixText: '', 776 | symbol: successTag 777 | }); 778 | 779 | await runPrismaCommand(`migrate status --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 780 | }); 781 | createSubCommand(migrate, "diff") 782 | .description("Compares the database schema from two arbitrary sources, and outputs the differences either as a human-readable summary (by default) or an executable script.") 783 | .option("--from-url [fromUrl]") 784 | .option("--to-url [toUrl]") 785 | 786 | .option("--from-empty") 787 | .option("--to-empty") 788 | 789 | .option("--from-schema-datamodel [fromDataModel]") 790 | .option("--to-schema-datamodel [toDataModel]") 791 | 792 | .option("--from-schema-datasource [fromDataSource]") 793 | .option("--to-schema-datasource [toDataSource]") 794 | 795 | .option("--from-migrations [fromMigrations]") 796 | .option("--to-migrations [toMigrations]") 797 | 798 | .option("--shadow-database-url [shadowDatabase]") 799 | 800 | .option("--script") 801 | .option("--exit-code") 802 | .action(async (options, command) => { 803 | options = command.optsWithGlobals(); 804 | 805 | if(options.H) 806 | { 807 | new MessageBuilder() 808 | .withHeader() 809 | .withTitle(chalk.gray("Compares the database schema from two arbitrary sources, and outputs the differences either as a human-readable summary (by default) or an executable script.")) 810 | .withNewLine() 811 | .withTitle(chalk.gray(`${chalk.blue("prisma-util migrate diff")} is a read-only command that does not write to your datasource(s).`)) 812 | .withTitle(chalk.gray(`${chalk.blue("prisma-util db execute")} can be used to execute its ${chalk.blue("--script")} output.`)) 813 | .withNewLine() 814 | .withTitle(chalk.gray(`The command takes a source ${chalk.blue("--from-...")} and a destination ${chalk.blue("--to-...")}.`)) 815 | .withTitle(chalk.gray(`The source and destination must use the same provider,`)) 816 | .withTitle(chalk.gray(`e.g. a diff using 2 different providers like PostgreSQL and SQLite is not supported.`)) 817 | .withNewLine() 818 | .withTitle(chalk.gray("It compares the source with the destination to generate a diff.")) 819 | .withTitle(chalk.gray("The diff can be interpreted as generating a migration that brings the source schema (from) to the shape of the destination schema (to).")) 820 | .withTitle(chalk.gray(`The default output is a human readable diff, it can be rendered as SQL using ${chalk.blue("--script")} on SQL databases.`)) 821 | .withNewLine() 822 | .withTitle(chalk.gray("See the documentation for more information https://pris.ly/d/migrate-diff")) 823 | .withNewLine() 824 | .withSection("Usage", [`${chalk.gray("$")} prisma-util migrate diff [options]`]) 825 | .withSection("Options", 826 | [ 827 | `${chalk.gray("-h, --help")} Display this help message`, "", 828 | chalk.italic("From and To inputs (1 `--from-...` and 1 `--to-...` must be provided):"), 829 | `${chalk.gray("--from-url")} A datasource URL`, 830 | chalk.gray("--to-url"), "", 831 | `${chalk.gray("--from-empty")} Flag to assume from or to is an empty datamodel`, 832 | chalk.gray("--to-empty"), "", 833 | `${chalk.gray("--from-schema-datamodel")} Path to a Prisma schema file, uses the ${chalk.italic("datamodel")} for the diff`, 834 | `${chalk.gray("--to-schema-datamodel")} You can also use ${chalk.blue("base")} for your base schema and ${chalk.blue("generated")} for the generated one`, "", 835 | `${chalk.gray("--from-schema-datasource")} Path to a Prisma schema file, uses the ${chalk.italic("datasource url")} for the diff`, 836 | `${chalk.gray("--to-schema-datasource")} You can also use ${chalk.blue("base")} for your base schema and ${chalk.blue("generated")} for the generated one`, "", 837 | `${chalk.gray("--from-migrations")} Path to the Prisma Migrate migrations directory`, 838 | chalk.gray("--to-migrations"), "", 839 | chalk.italic("Shadow database (only required if using --from-migrations or --to-migrations):"), 840 | `${chalk.gray("--shadow-database-url")} URL for the shadow database`, 841 | ]) 842 | .withSection("Flags", 843 | [ 844 | `${chalk.gray("--script")} Render a SQL script to stdout instead of the default human readable summary (not supported on MongoDB)`, 845 | `${chalk.gray("--exit-code")} Change the exit code behavior to signal if the diff is empty or not (Empty: 0, Error: 1, Not empty: 2). Default behavior is Success: 0, Error: 1.`, 846 | ]) 847 | .withSection("Examples", 848 | [ 849 | chalk.gray("From database to database as summary"), chalk.gray(" e.g. compare two live databases"), `${chalk.gray("$")} prisma-util migrate diff \\`, " --from-url \"postgresql://login:password@localhost:5432/db1\" \\", " --to-url \"postgresql://login:password@localhost:5432/db2\" \\", "", 850 | chalk.gray("From a live database to a Prisma datamodel"), chalk.gray(" e.g. roll forward after a migration failed in the middle"), `${chalk.gray("$")} prisma-util migrate diff \\`, " --shadow-database-url \"postgresql://login:password@localhost:5432/db1\" \\", " --from-url \"postgresql://login:password@localhost:5432/db2\" \\", " --to-schema-datamodel=next_datamodel.prisma \\", " --script", "", 851 | chalk.gray("From a live database to a datamodel"), chalk.gray(" e.g. roll backward after a migration failed in the middle"), `${chalk.gray("$")} prisma-util migrate diff \\`, " --shadow-database-url \"postgresql://login:password@localhost:5432/db1\" \\", " --from-url \"postgresql://login:password@localhost:5432/db2\" \\", " --to-schema-datamodel=previous_datamodel.prisma \\", " --script", "", 852 | chalk.gray(`From a Prisma Migrate ${chalk.blue("migrations")} directory to another database`), chalk.gray(" e.g. generate a migration for a hotfix already applied on production"), `${chalk.gray("$")} prisma-util migrate diff \\`, " --shadow-database-url \"postgresql://login:password@localhost:5432/db1\" \\", " --from-migrations ./migrations \\", " --to-url \"postgresql://login:password@localhost:5432/db2\" \\", " --script", "", 853 | chalk.gray("Detect if both sources are in sync, it will exit with exit code 2 if changes are detected"), `${chalk.gray("$")} prisma-util migrate diff \\`, " --exit-code \\", " --from-[...] \\", " --to-[...]" 854 | ]) 855 | .show(); 856 | return; 857 | } 858 | 859 | const shouldGenerate = [options.fromSchemaDatamodel, options.toSchemaDatamodel, options.fromSchemaDatasource, options.toSchemaDatasource].includes("generated"); 860 | 861 | if(shouldGenerate) 862 | { 863 | parser = await parser.load(); 864 | 865 | await fixConflicts(); 866 | 867 | console.log(); 868 | const spinner = ora({ 869 | text: `${chalk.gray("Generating merged schema...")}`, 870 | prefixText: prismaCLITag 871 | }).start(); 872 | await parser.writeSchema(); 873 | spinner.stopAndPersist({ 874 | text: `${chalk.gray("Merged schema generated successfully.")}`, 875 | prefixText: '', 876 | symbol: successTag 877 | }); 878 | 879 | options.fromSchemaDatamodel = options.fromSchemaDatamodel == "generated" ? "./node_modules/.bin/generated-schema.prisma" : options.fromSchemaDatamodel; 880 | options.toSchemaDatamodel = options.toSchemaDatamodel == "generated" ? "./node_modules/.bin/generated-schema.prisma" : options.toSchemaDatamodel; 881 | options.fromSchemaDatasource = options.fromSchemaDatasource == "generated" ? "./node_modules/.bin/generated-schema.prisma" : options.fromSchemaDatasource; 882 | options.toSchemaDatasource = options.toSchemaDatasource == "generated" ? "./node_modules/.bin/generated-schema.prisma" : options.toSchemaDatasource; 883 | } 884 | 885 | options.fromSchemaDatamodel = options.fromSchemaDatamodel == "base" ? parser.config.baseSchema : options.fromSchemaDatamodel; 886 | options.toSchemaDatamodel = options.toSchemaDatamodel == "base" ? parser.config.baseSchema : options.toSchemaDatamodel; 887 | options.fromSchemaDatasource = options.fromSchemaDatasource == "base" ? parser.config.baseSchema : options.fromSchemaDatasource; 888 | options.toSchemaDatasource = options.toSchemaDatasource == "base" ? parser.config.baseSchema : options.toSchemaDatasource; 889 | 890 | await runPrismaCommand(`migrate diff${options.fromSchemaDatamodel ? ` --from-schema-datamodel ${options.fromSchemaDatamodel}` : ""}${options.toSchemaDatamodel ? ` --to-schema-datamodel ${options.toSchemaDatamodel}` : ""}${options.fromSchemaDatasource ? ` --from-schema-datasource ${options.fromSchemaDatasource}` : ""}${options.toSchemaDatasource ? ` --to-schema-datasource ${options.toSchemaDatasource}` : ""}${options.fromMigrations ? ` --from-migrations ${options.fromMigrations}` : ""}${options.toMigrations ? ` --to-migrations ${options.toMigrations}` : ""}${options.shadowDatabaseUrl ? ` --shadow-database-url ${options.shadowDatabaseUrl}` : ""}${options.script ? " --script" : ""}${options.exitCode ? " --exit-code" : ""}${options.fromEmpty ? " --from-empty" : ""}${options.toEmpty ? " --to-empty" : ""}${options.fromUrl ? ` --from-url ${options.fromUrl}` : ""}${options.toUrl ? ` --to-url ${options.toUrl}` : ""}${options.previewFeature ? " --preview-feature" : ""}`); 891 | }); 892 | 893 | // Match Prisma's studio command 894 | program 895 | .command('studio') 896 | .description('Browse your data with Prisma Studio') 897 | .option('-p, --port [port]', "Port to start Studio on", compositeTypeDepthParser, "5555") 898 | .option('-b, --browser [browser]', "Browser to open Studio in") 899 | .option('-n, --hostname', "Hostname to bind the Express server to") 900 | .action(async (options) => { 901 | options.port = parseInt(options.port, 10); 902 | 903 | if (options.H) { 904 | new MessageBuilder() 905 | .withHeader() 906 | .withTitle(chalk.gray("Browse your data with Prisma Studio")) 907 | .withNewLine() 908 | .withSection("Usage", [`${chalk.gray("$")} prisma-util studio [options]`]) 909 | .withSection("Options", 910 | [` ${chalk.gray("-h, --help")} Display this help message`, 911 | ` ${chalk.gray("-p, --port")} Port to start Studio on`, 912 | ` ${chalk.gray("-b, --browser")} Browser to open Studio in`, 913 | `${chalk.gray("-n, --hostname")} Hostname to bind the Express server to` 914 | ]) 915 | .withSection("Examples", 916 | [chalk.gray("Start Studio on the default port"), `${chalk.gray("$")} prisma-util studio`, "", 917 | chalk.gray("Start Studio on a custom port"), `${chalk.gray("$")} prisma-util studio --port 5555`, "", 918 | chalk.gray("Start Studio in a specific browser"), `${chalk.gray("$")} prisma-util studio --port 5555 --browser firefox`, "", 919 | chalk.gray("Start Studio without opening in a browser"), `${chalk.gray("$")} prisma-util studio --port 5555 --browser none` 920 | ]) 921 | .show(); 922 | return; 923 | } 924 | 925 | parser = await parser.load(); 926 | 927 | await fixConflicts(); 928 | 929 | console.log(); 930 | const spinner = ora({ 931 | text: `${chalk.gray("Generating merged schema...")}`, 932 | prefixText: prismaCLITag 933 | }).start(); 934 | await parser.writeSchema(); 935 | spinner.stopAndPersist({ 936 | text: `${chalk.gray("Merged schema generated successfully.")}`, 937 | prefixText: '', 938 | symbol: successTag 939 | }); 940 | 941 | await runPrismaCommand(`studio --port ${options.port}${options.browser ? ` --browser ${options.browser}` : ""}${options.hostname ? ` --hostname ${options.hostname}` : ""} --schema ./node_modules/.bin/generated-schema.prisma${options.previewFeature ? " --preview-feature" : ""}`); 942 | }); 943 | 944 | // Add Prisma Util and Prisma flags to all commands. 945 | program.commands.forEach((cmd) => { 946 | cmd.option("--config [config]", "Specify a different path for the Prisma Util config", "") 947 | .option("--help, -h", "Display this help message") 948 | .option("--preview-feature", "Run Preview Prisma commands") 949 | }); 950 | 951 | // Run the commands 952 | program.parse(); 953 | 954 | async function fixConflicts(iterationCount = 0) { 955 | return new Promise(async (resolve) => { 956 | let conflicts = await parser.getConflicts(); 957 | 958 | if (conflicts.length == 0) { 959 | success("All conflicts resolved, proceeding with command.", "\n"); 960 | resolve(); 961 | } else { 962 | if (iterationCount == 0) 963 | conflict("Conflicts detected, please answer the questions below.", "\n"); 964 | const conflictNow = { 965 | 1: conflicts[0][1].name, 966 | 2: conflicts[0][2].name 967 | }; 968 | const conflictNowTypes = { 969 | 1: conflicts[0][1].type, 970 | 2: conflicts[0][2].type, 971 | } 972 | 973 | // Both should be the same 974 | const referred1 = parser.getReferredRelations(conflictNow[1]); 975 | const referred2 = parser.getReferredRelations(conflictNow[2]); 976 | if ((referred1.length > 0 || referred2.length > 0) && !parser.config.crossFileRelations) { 977 | error(`Cross-file relations are not enabled in ${chalk.bold(parser.configPath)}.\n`, `\n`) 978 | process.exit(1); 979 | } 980 | 981 | // Try to fix with config file 982 | const canMap = { 983 | 1: false, 984 | 2: false 985 | } 986 | referred1.forEach((ref) => { 987 | const res = parser.canFixCrossFileWithMapper(`${ref.model}.${ref.column.name}`); 988 | if (res) { 989 | parser.suggest(conflictNow[1], { 990 | type: "remap", 991 | from: `${ref.model}.${ref.column.name}`, 992 | to: res, 993 | item: conflictNowTypes[1] 994 | }); 995 | canMap[1] = conflictNow[1] == res; 996 | canMap[2] = conflictNow[2] == res; 997 | } 998 | }); 999 | 1000 | // If there is another one, ask the user for help 1001 | const canMapAny = canMap[1] || canMap[2]; 1002 | if (canMapAny) { 1003 | const mapper = canMap[1] ? { 1004 | name: conflictNow[1], 1005 | type: conflictNowTypes[1] 1006 | } : { 1007 | name: conflictNow[2], 1008 | type: conflictNowTypes[2] 1009 | }; 1010 | const other = canMap[1] ? { 1011 | name: conflictNow[2], 1012 | type: conflictNowTypes[2] 1013 | } : { 1014 | name: conflictNow[1], 1015 | type: conflictNowTypes[1] 1016 | }; 1017 | experimental(`The ${chalk.bold("Automatic Mapper")} can't process a conflict automatically.\n`, "\n"); 1018 | const answers = await inquirer.prompt({ 1019 | name: `resolver_${iterationCount}`, 1020 | type: 'list', 1021 | prefix: conflictTag, 1022 | message: chalk.gray(`Review your schema, then choose an option to solve the conflict.\n\n${chalk.magenta(`${mapper.name}${mapper.type == "enum" ? " (Enum)" : ""}`)} is referenced in your configuration file as the replacement for another model.\nHowever, ${chalk.magenta(`${other.name}${other.type == "enum" ? " (Enum)" : ""}`)} has the same model name as the generated one would.\nPlease choose one of the options below.\n\n${chalk.gray("Your choice:")}`), 1023 | choices: [ 1024 | `Skip ${chalk.magenta(`${other.name}${other.type == "enum" ? " (Enum)" : ""}`)}`, 1025 | `Rename ${chalk.magenta(`${other.name}${other.type == "enum" ? " (Enum)" : ""}`)}`, 1026 | ], 1027 | }); 1028 | const answer = answers[`resolver_${iterationCount}`].replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ""); 1029 | switch (answer) { 1030 | case `Skip ${other.name}${other.type == "enum" ? " (Enum)" : ""}`: 1031 | parser.suggest(other.name, { type: "skip", item: other.type }) 1032 | break; 1033 | case `Rename ${other.name}${other.type == "enum" ? " (Enum)" : ""}`: 1034 | const name1 = (await inquirer.prompt({ 1035 | name: `resolver_rename_${iterationCount}`, 1036 | type: 'input', 1037 | prefix: conflictTag, 1038 | message: chalk.gray(`What is the new name for ${chalk.magenta(`${other.name}${other.type == "enum" ? " (Enum)" : ""}`)}?`), 1039 | }))[`resolver_rename_${iterationCount}`]; 1040 | 1041 | parser.suggest(other.name, { 1042 | type: "rename", 1043 | newName: name1, item: other.type 1044 | }); 1045 | break; 1046 | } 1047 | resolve(fixConflicts(iterationCount + 1)); 1048 | return; 1049 | } 1050 | 1051 | // Show never be shown unless the json is parsed incorrectly 1052 | const warningText = canMap[1] && canMap[2] ? `${chalk.yellow("Warning: ")}Both ${chalk.magenta(`${conflictNow[1]}${conflictNowTypes[1] == "enum" ? " (Enum)" : ""}`)} and ${chalk.magenta(`${conflictNow[2]}${conflictNowTypes[2] == "enum" ? " (Enum)" : ""}`)} are mapping the same column.\n\n` : "" 1053 | console.log(); 1054 | const answers = await inquirer.prompt({ 1055 | name: `resolver_${iterationCount}`, 1056 | type: 'list', 1057 | prefix: conflictTag, 1058 | message: chalk.gray(`Review your schema, then choose an option to solve the conflict.\n\nTwo models have the same name, please select an action.\n${chalk.magenta(`${conflictNow[1]}${conflictNowTypes[1] == "enum" ? " (Enum)" : ""}`)} and ${chalk.magenta(`${conflictNow[2]}${conflictNowTypes[2] == "enum" ? " (Enum)" : ""}`)}\n\n${warningText}${chalk.gray("Your choice:")}`), 1059 | choices: [ 1060 | `Skip ${chalk.magenta(`${conflictNow[1]}${conflictNowTypes[1] == "enum" ? " (Enum)" : ""}`)}`, 1061 | `Skip ${chalk.magenta(`${conflictNow[2]}${conflictNowTypes[2] == "enum" ? " (Enum)" : ""}`)}`, 1062 | `Rename ${chalk.magenta(`${conflictNow[1]}${conflictNowTypes[1] == "enum" ? " (Enum)" : ""}`)}`, 1063 | `Rename ${chalk.magenta(`${conflictNow[2]}${conflictNowTypes[2] == "enum" ? " (Enum)" : ""}`)}`, 1064 | ], 1065 | }); 1066 | 1067 | // remove colors 1068 | const answer = answers[`resolver_${iterationCount}`].replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ""); 1069 | switch (answer) { 1070 | case `Skip ${conflictNow[1]}${conflictNowTypes[1] == "enum" ? " (Enum)" : ""}`: 1071 | parser.suggest(conflictNow[1], { type: "skip", item: conflictNowTypes[1] }) 1072 | break; 1073 | case `Skip ${conflictNow[2]}${conflictNowTypes[2] == "enum" ? " (Enum)" : ""}`: 1074 | parser.suggest(conflictNow[2], { type: "skip", item: conflictNowTypes[2] }) 1075 | break; 1076 | case `Rename ${conflictNow[1]}${conflictNowTypes[1] == "enum" ? " (Enum)" : ""}`: 1077 | const name1 = (await inquirer.prompt({ 1078 | name: `resolver_rename_${iterationCount}`, 1079 | type: 'input', 1080 | prefix: conflictTag, 1081 | message: chalk.gray(`What is the new name for ${chalk.magenta(`${conflictNow[1]}${conflictNowTypes[1] == "enum" ? " (Enum)" : ""}`)}?`), 1082 | }))[`resolver_rename_${iterationCount}`]; 1083 | 1084 | parser.suggest(conflictNow[1], { 1085 | type: "rename", 1086 | newName: name1, 1087 | item: conflictNowTypes[1] 1088 | }) 1089 | break; 1090 | case `Rename ${conflictNow[2]}${conflictNowTypes[2] == "enum" ? " (Enum)" : ""}`: 1091 | const name2 = (await inquirer.prompt({ 1092 | name: `resolver_rename_${iterationCount}`, 1093 | type: 'input', 1094 | prefix: conflictTag, 1095 | message: chalk.gray(`What is the new name for ${chalk.magenta(`${conflictNow[2]}${conflictNowTypes[2] == "enum" ? " (Enum)" : ""}`)}?`), 1096 | }))[`resolver_rename_${iterationCount}`]; 1097 | 1098 | parser.suggest(conflictNow[2], { 1099 | type: "rename", 1100 | newName: name2, 1101 | item: conflictNowTypes[2] 1102 | }) 1103 | break; 1104 | } 1105 | 1106 | resolve(fixConflicts(iterationCount + 1)); 1107 | } 1108 | }) 1109 | } 1110 | 1111 | // Sub command helper 1112 | function createSubCommand(command: commander.Command, nameAndArgs: string) { 1113 | const subCommand = command.command(nameAndArgs); 1114 | return subCommand; 1115 | } 1116 | -------------------------------------------------------------------------------- /src/cli/interactive.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { error, interactive } from "./logger.js"; 3 | import gradient from "gradient-string"; 4 | import MessageBuilder from "./messages.js"; 5 | import ora from "ora"; 6 | import inquirer from "inquirer"; 7 | import GitCreator, { ROOT } from './lib/git.js'; 8 | import * as fs from 'fs/promises'; 9 | import { convertPathToLocal } from "./utils.js"; 10 | import * as child_process from 'child_process'; 11 | 12 | type Text = string | { 13 | color: string; 14 | text: string; 15 | }; 16 | 17 | type AssetPath = `${string} as ${string}` | `mkdir ${string}`; 18 | 19 | type TutorialStep = ({ 20 | type: "editFile"; 21 | path: string; 22 | } | { 23 | type: "removeFile"; 24 | path: string; 25 | } | { 26 | type: "command"; 27 | allowedCommand: string; 28 | displayPath: string; 29 | changeEnv: NodeJS.ProcessEnv 30 | }) & { 31 | text: Text[][]; 32 | }; 33 | 34 | type TutorialManifest = { 35 | $manifest: { 36 | assets: AssetPath[]; 37 | }; 38 | steps: TutorialStep[]; 39 | }; 40 | 41 | export default class InteractiveMode { 42 | private example: string; 43 | private git: ReturnType; 44 | private manifest?: TutorialManifest; 45 | constructor(example?: string) 46 | { 47 | console.clear(); 48 | this.example = example ? example : ""; 49 | this.git = GitCreator(ROOT); 50 | 51 | const builder = new MessageBuilder() 52 | .withHeader(gradient.passion("Interactive Mode")) 53 | .withTitle(chalk.gray("This mode allows you to follow step-by-step tutorials using the CLI.")); 54 | 55 | if(!example) 56 | { 57 | builder 58 | .withNewLine() 59 | .withSection(gradient.passion("How to start?"), [ 60 | `${chalk.gray("Run a tutorial from the Documentation")}`, `${chalk.gray("$")} prisma-util interactive --tutorial `, "", 61 | `${chalk.gray("See a list of available tutorials")}`, `https://prisma-util.gitbook.io/stable/api-documentation/command-reference/interactive#available-tutorials` 62 | ], true) 63 | .show() 64 | return; 65 | } 66 | 67 | builder.show(); 68 | 69 | this.queue(); 70 | } 71 | 72 | async queue() 73 | { 74 | const manifest = await this.git.File.get("examples", `${this.example}.json`); 75 | if(manifest === undefined) 76 | { 77 | new MessageBuilder() 78 | .withHeader(gradient.passion("Tutorial Error")) 79 | .withTitle(chalk.gray("There has been an error while trying to fetch the tutorial manifest.")) 80 | .show(); 81 | return; 82 | }; 83 | this.manifest = manifest; 84 | 85 | const res = await inquirer.prompt({ 86 | type: "confirm", 87 | name: "run", 88 | message: chalk.gray("This tutorial will download files and install packages to this directory.\nDo you agree to run this example?"), 89 | prefix: gradient.passion("?"), 90 | }); 91 | if(res.run) 92 | { 93 | console.log(); 94 | await this.download(); 95 | 96 | return; 97 | } 98 | 99 | this.end(); 100 | } 101 | 102 | async download() 103 | { 104 | const spinner = ora({ 105 | text: chalk.gray("Hang on while we set everything up for this tutorial...\nThe console output will be cleared once everything is ready.\n"), 106 | spinner: { 107 | "interval": 80, 108 | "frames": [ 109 | gradient.passion("⠋"), 110 | gradient.passion("⠙"), 111 | gradient.passion("⠹"), 112 | gradient.passion("⠸"), 113 | gradient.passion("⠼"), 114 | gradient.passion("⠴"), 115 | gradient.passion("⠦"), 116 | gradient.passion("⠧"), 117 | gradient.passion("⠇"), 118 | gradient.passion("⠏") 119 | ] 120 | } 121 | }); 122 | spinner.start(); 123 | 124 | if(!this.manifest) 125 | { 126 | spinner.stop(); 127 | this.end(); 128 | 129 | return; 130 | } 131 | 132 | for(const asset of this.manifest.$manifest.assets) 133 | { 134 | if(asset.startsWith("mkdir")) 135 | { 136 | await fs.mkdir(asset.split("mkdir ")[1], { recursive: true }); 137 | } else 138 | { 139 | const pathParts = asset.split(" as "); 140 | await fs.writeFile(convertPathToLocal(pathParts[1]), await this.git.File.get("examples", pathParts[0]) ?? ""); 141 | } 142 | } 143 | 144 | spinner.stop(); 145 | 146 | await this.emptyLoop(); 147 | } 148 | 149 | async emptyLoop() 150 | { 151 | let i = -1; 152 | let currentStep = 0; 153 | let stepIteration = 0; 154 | 155 | if(!this.manifest) 156 | { 157 | return; 158 | } 159 | 160 | let err: string | null = null; 161 | 162 | while(true) 163 | { 164 | i++; 165 | this.clearWindow(); 166 | 167 | console.log("\n"); 168 | 169 | if(currentStep >= this.manifest.steps.length) 170 | { 171 | new MessageBuilder() 172 | .withTitle(`${chalk.gray("This tutorial has been finished. You may only run")} ${gradient.passion(".exit")} ${chalk.gray("and")} ${gradient.passion(".cleanup")}${chalk.gray(".")}`) 173 | .show(); 174 | const res = await inquirer.prompt({ 175 | type: "input", 176 | name: `run_command_${i}`, 177 | prefix: gradient.passion("$"), 178 | message: chalk.gray("root:"), 179 | }); 180 | switch(res[`run_command_${i}`]) 181 | { 182 | case '.exit': 183 | this.end(); 184 | process.exit(0); 185 | break; 186 | case '.cleanup': 187 | if(this.manifest) 188 | { 189 | for(const file of this.manifest.$manifest.assets) 190 | { 191 | try { 192 | if(file.startsWith("mkdir")) 193 | { 194 | await fs.rmdir(file.split("mkdir ")[1]); 195 | } else 196 | { 197 | const as = file.split(" as ")[1]; 198 | await fs.rm(as); 199 | } 200 | } catch {} 201 | } 202 | } 203 | this.end(); 204 | process.exit(0); 205 | break; 206 | } 207 | continue; 208 | } 209 | 210 | const step = this.manifest.steps[currentStep]; 211 | 212 | let builder = new MessageBuilder() 213 | .withHeader(gradient.passion(`Interactive Mode (Step ${currentStep + 1} / ${this.manifest.steps.length})`)); 214 | for(const lines of step.text) 215 | { 216 | const text = lines.map(line => { 217 | return typeof line == "string" ? chalk.gray(line) : ( 218 | line.color == "passion" ? gradient.passion(line.text) : (chalk as any)[line.color](line.text)); 219 | }).join(""); 220 | builder = builder.withTitle(text); 221 | } 222 | builder.show(); 223 | if(err) 224 | { 225 | error(err, "\n"); 226 | } 227 | 228 | switch(step.type) 229 | { 230 | case 'removeFile': 231 | const removeRes = await inquirer.prompt({ 232 | type: "confirm", 233 | name: `confirm_${currentStep}_${stepIteration}`, 234 | message: chalk.gray("Have you removed the file according to the tutorial?"), 235 | prefix: gradient.passion("?"), 236 | }); 237 | if(removeRes[`confirm_${currentStep}_${stepIteration}`]) 238 | { 239 | try { 240 | await fs.access(step.path); 241 | err = `The ${chalk.bold(step.path)} file is still present.`; 242 | } catch { 243 | err = null; 244 | currentStep++; 245 | stepIteration = 0; 246 | } 247 | } else 248 | { 249 | stepIteration++; 250 | } 251 | break; 252 | case 'editFile': 253 | const editRes = await inquirer.prompt({ 254 | type: "confirm", 255 | name: `confirm_${currentStep}_${stepIteration}`, 256 | message: chalk.gray("Have you edited the file according to the tutorial?"), 257 | prefix: gradient.passion("?"), 258 | }); 259 | if(editRes[`confirm_${currentStep}_${stepIteration}`]) 260 | { 261 | err = null; 262 | currentStep++; 263 | stepIteration = 0; 264 | } else 265 | { 266 | stepIteration++; 267 | } 268 | break; 269 | case 'command': 270 | const res = await inquirer.prompt({ 271 | type: "input", 272 | name: `run_command_${currentStep}_${stepIteration}`, 273 | prefix: gradient.passion("$"), 274 | message: chalk.gray(`${step.displayPath}:`), 275 | }); 276 | const command = res[`run_command_${currentStep}_${stepIteration}`]; 277 | switch(command) 278 | { 279 | case '.exit': 280 | this.end(); 281 | process.exit(0); 282 | break; 283 | case '.cleanup': 284 | if(this.manifest) 285 | { 286 | for(const file of this.manifest.$manifest.assets) 287 | { 288 | try { 289 | if(file.startsWith("mkdir")) 290 | { 291 | await fs.rmdir(file.split("mkdir ")[1]); 292 | } else 293 | { 294 | const as = file.split(" as ")[1]; 295 | await fs.rm(as); 296 | } 297 | } catch {} 298 | } 299 | } 300 | this.end(); 301 | process.exit(0); 302 | break; 303 | default: 304 | if(command == step.allowedCommand) 305 | { 306 | await this.runCommand(command, step.changeEnv); 307 | err = null; 308 | currentStep++; 309 | stepIteration = 0; 310 | } else 311 | { 312 | stepIteration++; 313 | } 314 | break; 315 | } 316 | break; 317 | } 318 | } 319 | } 320 | 321 | async runCommand(command: string, env: NodeJS.ProcessEnv) 322 | { 323 | return new Promise((resolve) => { 324 | // Spawn a child process running the command. 325 | const proc = child_process.spawn(command, { 326 | stdio: 'inherit', 327 | shell: true, 328 | env: { 329 | ...process.env, 330 | ...env 331 | } 332 | }); 333 | 334 | proc.on("exit", (signal) => { 335 | // Resolve the promise on exit 336 | resolve(signal == 0); 337 | }); 338 | }) 339 | } 340 | 341 | clearWindow() 342 | { 343 | console.clear(); 344 | this.printControls(); 345 | } 346 | 347 | printControls() 348 | { 349 | new MessageBuilder() 350 | .withHeader(gradient.passion("Interactive Mode Controls")) 351 | .withTitle(chalk.gray(`At any time, type ${gradient.passion(".exit")} to exit the interactive mode.`)) 352 | .withTitle(chalk.gray(`If you want to cleanup files as well as exit the interactive mode, type ${gradient.passion(".cleanup")}.`)) 353 | .withNewLine() 354 | .withTitle(chalk.gray(`Any input that isn't a function specified above will be treated and executed as a command.`)) 355 | .show(); 356 | } 357 | 358 | end() 359 | { 360 | console.clear(); 361 | new MessageBuilder() 362 | .withHeader(gradient.passion("Interactive Mode Ended")) 363 | .withTitle(chalk.gray(`This interactive mode session has ended.`)) 364 | .show(); 365 | } 366 | } -------------------------------------------------------------------------------- /src/cli/lib/git.ts: -------------------------------------------------------------------------------- 1 | import os, { SignalConstants } from "os"; 2 | import axios, { AxiosError, AxiosResponse } from "axios"; 3 | 4 | type BranchResult = { 5 | name: string, 6 | commit: { 7 | sha: string, 8 | url: string 9 | }, 10 | protected: boolean 11 | }; 12 | 13 | type GitHubAPIResponse = { 14 | success: false, 15 | status: number 16 | } | { 17 | success: true, 18 | data: T 19 | }; 20 | 21 | export const ROOT = "DavidHancu/prisma-util"; 22 | 23 | const GitCreator = (root: string) => { 24 | return { 25 | Branch: { 26 | list: async () => { 27 | const res = await queryAxios(`https://api.github.com/repos/${root}/branches`); 28 | 29 | if(!res.success) 30 | return [] as string[]; 31 | 32 | return res.data.map(branch => branch.name); 33 | }, 34 | exists: async (branchName: string) => { 35 | const res = await queryAxios(`https://api.github.com/repos/${root}/branches/${branchName}`); 36 | 37 | return res.success; 38 | } 39 | }, 40 | File: { 41 | get: async (branch: string, file: string) => { 42 | const res = await queryAxios(`https://raw.githubusercontent.com/${root}/${branch}/${file}`); 43 | 44 | if(!res.success) 45 | return undefined; 46 | 47 | return res.data; 48 | } 49 | } 50 | }; 51 | }; 52 | 53 | function queryAxios(link: string): Promise> 54 | { 55 | return new Promise(async (resolve) => { 56 | await axios 57 | .get(link) 58 | .catch((axiosError: AxiosError) => { 59 | resolve({ 60 | success: false, 61 | status: axiosError.response?.status ? axiosError.response?.status : 404 62 | }); 63 | }) 64 | .then((axiosResponse: void | AxiosResponse) => { 65 | resolve({ 66 | success: true, 67 | data: (typeof axiosResponse == "object") ? axiosResponse.data as T : axiosResponse as T 68 | }); 69 | }); 70 | }) 71 | } 72 | 73 | export default GitCreator; -------------------------------------------------------------------------------- /src/cli/lib/toolchain/extensions.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../../logger.js"; 2 | import * as fs from 'fs/promises'; 3 | import { convertPathToLocal } from "../../utils.js"; 4 | import path from "path"; 5 | import crypto from "crypto"; 6 | import chalk from "chalk"; 7 | 8 | /** 9 | * A Feature is an extension that will be generated. 10 | */ 11 | export type Feature = { 12 | identifier: string; 13 | code: string; 14 | } 15 | /** 16 | * Packages are the scope wrappers for extensions. 17 | */ 18 | export type Package = { 19 | identifier: string; 20 | features: Feature[] 21 | } 22 | 23 | /** 24 | * Project Toolchain Extension API implementation. 25 | * 26 | * This class orchestrates extension generation and hashing and provides an unified API for using the generated code. 27 | * It provides an easy way of creating extensions and correct scoping to make sure that Prisma Util users have a smooth experience. 28 | * 29 | * This API is intended for internal use only. You should not instantiate this class, but rather use the exported 30 | * instance. ({@link ExtensionToolchainInstance}) 31 | */ 32 | class ExtensionToolchain { 33 | private processPromise?: Promise = undefined; 34 | private packages: { 35 | [identifier: string]: Package 36 | } = {}; 37 | 38 | constructor() {} 39 | 40 | /** 41 | * Add an extension to the current queue. 42 | * @param pack The package name. 43 | * @param name The name of this extension. 44 | * @param code The code for this extension. 45 | * @returns This instance for chaining. 46 | */ 47 | public defineExtension(pack: string, name: string, code: string) 48 | { 49 | if(this.packages[pack]) 50 | this.packages[pack].features.push({ 51 | identifier: name, 52 | code 53 | }); 54 | else 55 | this.packages[pack] = { 56 | identifier: pack, 57 | features: [{ 58 | identifier: name, 59 | code 60 | }] 61 | }; 62 | return this; 63 | } 64 | 65 | /** 66 | * Generate all of the extensions that are currently in the queue. 67 | */ 68 | public async generate() 69 | { 70 | if(this.processPromise) 71 | return this.processPromise; 72 | 73 | log("Prisma Util Toolchain is starting to process the extensions generation queue...", "\n"); 74 | 75 | this.processPromise = new Promise(async (resolve) => { 76 | const packageNames = Object.keys(this.packages); 77 | const generatedPackageCount: { 78 | [packageIdentifier: string]: number 79 | } = {}; 80 | 81 | const updateGeneratedRepository = (transaction: string) => { 82 | generatedPackageCount[transaction] = generatedPackageCount[transaction] ? generatedPackageCount[transaction] + 1 : 1; 83 | }; 84 | 85 | for(const file of (await fs.readdir(convertPathToLocal(path.join("node_modules", "prisma-util", "toolchain", "extensions")))).filter(dirent => !packageNames.includes(dirent))) 86 | { 87 | await fs.rm(convertPathToLocal(path.join("node_modules", "prisma-util", "toolchain", "extensions", file)), { recursive: true, force: true }); 88 | } 89 | for(const pack of Object.entries(this.packages)) 90 | { 91 | const [packageName, packageData] = pack; 92 | const featureNames = packageData.features.filter(feature => !["all", "default"].includes(feature.identifier)).map(feature => feature.identifier); 93 | if(featureNames.length == 0) 94 | continue; 95 | 96 | let existing = true; 97 | 98 | const packageRoot = convertPathToLocal(path.join("node_modules", "prisma-util", "toolchain", "extensions", packageName)); 99 | try { 100 | await fs.access(packageRoot); 101 | } catch (err) { 102 | existing = false; 103 | await fs.mkdir(packageRoot); 104 | } 105 | 106 | const generatedRoot = path.join(packageRoot, "generated"); 107 | const extensionsLines = 108 | featureNames.length == 1 ? 109 | `export default function extension(prisma: PrismaClient): Omit;` : 110 | `export default extensionsBase; 111 | ${featureNames.map(feature => { 112 | return `import ${feature} from "./generated/${feature}.js";\ndeclare const ${feature}: (prisma: PrismaClient) => Omit;`; 113 | }).join("\n")} 114 | export { ${featureNames.join(", ")} }; 115 | `; 116 | const indexDTS = 117 | `import { PrismaClient } from '@prisma/client'; 118 | declare type AvailableExtensions = ${featureNames.map(feature => `"${feature}"`).join(" | ")}; 119 | declare type ExportedExtensionsBase = { 120 | [extensionName in AvailableExtensions]: (prisma: PrismaClient) => Omit; 121 | } & { 122 | /** 123 | * Add all extensions defined in this folder. 124 | * 125 | * @param prisma The instance of the PrismaClient that will be modified. 126 | * @returns The PrismaClient with all extensions added. 127 | */ 128 | all: (prisma: PrismaClient) => Omit; 129 | }; 130 | declare const extensionsBase: ExportedExtensionsBase; 131 | 132 | ${extensionsLines}`; 133 | await fs.writeFile(path.join(packageRoot, "index.d.ts"), indexDTS); 134 | 135 | const indexJS = 136 | featureNames.length == 1 ? 137 | `import ${featureNames[0]} from "./generated/${featureNames[0]}.js"; 138 | export default ${featureNames[0]};` : 139 | `${featureNames.map(feature => { 140 | return `import ${feature} from "./generated/${feature}.js";` 141 | }).join("\n")} 142 | 143 | const extensionsBase = { 144 | all: (prisma) => { 145 | ${featureNames.map(feature => { 146 | return `prisma = ${feature}(prisma);`; 147 | }).join("\n")} 148 | return prisma; 149 | }, 150 | ${featureNames.join(", ")} 151 | }; 152 | 153 | export default extensionsBase; 154 | export { ${featureNames.join(", ")} };`; 155 | 156 | await fs.writeFile(path.join(packageRoot, "index.js"), indexJS); 157 | if(!existing) 158 | { 159 | await fs.mkdir(generatedRoot); 160 | 161 | for(const feature of packageData.features) 162 | { 163 | await fs.writeFile(`${path.join(generatedRoot, feature.identifier)}.js`, feature.code); 164 | updateGeneratedRepository(packageName); 165 | } 166 | 167 | continue; 168 | } 169 | 170 | const paths = (await fs.readdir(generatedRoot)); 171 | for(const file of paths.filter(dirent => !featureNames.includes(path.parse(dirent).name))) 172 | { 173 | await fs.rm(path.join(generatedRoot, file), { recursive: true, force: true }); 174 | } 175 | for(const { identifier, code } of packageData.features.filter(feature => paths.includes(`${feature.identifier}.js`))) 176 | { 177 | const fileBuffer = await fs.readFile(`${path.join(generatedRoot, identifier)}.js`); 178 | 179 | const currentHashSum = crypto.createHash('sha256'); 180 | currentHashSum.update(fileBuffer); 181 | const currentHex = currentHashSum.digest('hex'); 182 | 183 | const newHashSum = crypto.createHash('sha256'); 184 | newHashSum.update(code); 185 | const newHex = newHashSum.digest('hex'); 186 | 187 | if(currentHex == newHex) 188 | continue; 189 | 190 | await fs.writeFile(`${path.join(generatedRoot, identifier)}.js`, code); 191 | updateGeneratedRepository(packageName); 192 | } 193 | 194 | for(const { identifier, code } of packageData.features.filter(feature => !paths.includes(`${feature.identifier}.js`))) 195 | { 196 | await fs.writeFile(`${path.join(generatedRoot, identifier)}.js`, code); 197 | updateGeneratedRepository(packageName); 198 | } 199 | } 200 | 201 | log(Object.keys(generatedPackageCount).length > 0 ? 202 | `Prisma Util Toolchain has processed the following extensions: 203 | ${Object.entries(this.packages).map(pack => { 204 | return `${chalk.blue(pack[0])}: ${pack[1].features.map(feature => chalk.white(feature.identifier)).join(", ")}`; 205 | }).join("\n")}` : "Prisma Util Toolchain didn't generate any extensions."); 206 | resolve(); 207 | }); 208 | return this.processPromise; 209 | } 210 | } 211 | 212 | /** 213 | * Instance of the Project Toolchain Extensions API Implementation. 214 | * 215 | * This is the entry-point to all extension creation, as it provides an unified interface for generating scoped 216 | * extensions that are easy to use. 217 | */ 218 | export const ExtensionsToolchainInstance = new ExtensionToolchain(); -------------------------------------------------------------------------------- /src/cli/lib/toolchain/generation.ts: -------------------------------------------------------------------------------- 1 | import { convertPathToLocal, normalizePath } from "../../utils.js"; 2 | import * as fs from 'fs/promises'; 3 | import { log } from '../../logger.js'; 4 | import chalk from "chalk"; 5 | 6 | /** 7 | * Project Toolchain asset representing a PrismaClient file and its name inside the Generation Toolchain. 8 | * This type is used to initialize the Generation Toolchain. 9 | * 10 | * The key of this object represents the code name of this asset. 11 | * The value of this object represents the path of the asset relative to the project root. 12 | */ 13 | type AssetBundle = { 14 | [key: string]: string | AssetBundle 15 | }; 16 | 17 | /** 18 | * Safe null for strings. 19 | */ 20 | const SAFE_NULL_STRING = ""; 21 | 22 | /** 23 | * Ignore search step. 24 | */ 25 | export const IGNORE_SEARCH_STEP = ""; 26 | 27 | /** 28 | * Check if a string is valid. 29 | * @param checked The string to check. 30 | * @returns Whether the checked string is valid or not. 31 | */ 32 | function validString(checked?: string): boolean { 33 | return !(!checked) && checked != SAFE_NULL_STRING; 34 | } 35 | 36 | /** 37 | * Project Toolchain default assets to be used in the Generation Toolchain. 38 | * 39 | * This map includes all runtime declarations of `@prisma/client/runtime`: 40 | * 41 | * PRISMA_CLIENT_RUNTIME: { 42 | * EDGE: "node_modules/@prisma/client/runtime/edge.js", 43 | * EDGE_ESM: "node_modules/@prisma/client/runtime/edge-esm.js", 44 | * INDEX: "node_modules/@prisma/client/runtime/index.js", 45 | * } 46 | * 47 | * This map includes all generated declarations of `.prisma/client`: 48 | * 49 | * PRISMA_CLIENT_GENERATED: { 50 | * EDGE: "node_modules/.prisma/client/edge.js", 51 | * INDEX: "node_modules/.prisma/client/index.js", 52 | * INDEX_TYPES: "node_modules/.prisma/client/index.d.ts", 53 | * } 54 | */ 55 | const DEFAULT_ASSETS = { 56 | PRISMA_CLIENT_RUNTIME: { 57 | EDGE: "node_modules/@prisma/client/runtime/edge.js", 58 | EDGE_ESM: "node_modules/@prisma/client/runtime/edge-esm.js", 59 | INDEX: "node_modules/@prisma/client/runtime/index.js", 60 | }, 61 | PRISMA_CLIENT_GENERATED: { 62 | EDGE: "node_modules/.prisma/client/edge.js", 63 | INDEX: "node_modules/.prisma/client/index.js", 64 | INDEX_TYPES: "node_modules/.prisma/client/index.d.ts", 65 | } 66 | } 67 | 68 | /** 69 | * Escape a search query. 70 | * @param string The string to escape. 71 | * @returns Escaped string to use inside of regex. 72 | */ 73 | function escapeRegex(string: string) { 74 | return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 75 | } 76 | 77 | /** 78 | * The strategy that the Generation Toolchain should use for an {@link EditTransaction}. 79 | */ 80 | export enum EditStrategy { 81 | REGEX, JOIN, REPLACE, REPLACE_UNSAFE, REPLACE_FULL, NONE 82 | } 83 | 84 | /** 85 | * Project Toolchain backend implementation. 86 | * 87 | * This class orchestrates code generation and editing and provides an unified API for using the generated code. 88 | * It provides utilities as well as state handling between edits to make sure that the correct code is written. 89 | * 90 | * This API is intended for internal use only. You should not instantiate this class, but rather use the exported 91 | * instance. ({@link GenerationToolchainInstance}) 92 | */ 93 | class GenerationToolchain { 94 | private _assets: AssetBundle & typeof DEFAULT_ASSETS = DEFAULT_ASSETS; 95 | private queue: EditTransaction[] = []; 96 | private processPromise?: Promise = undefined; 97 | private _useExtensions: boolean = false; 98 | 99 | constructor() { 100 | 101 | } 102 | 103 | /** 104 | * Use extensions instead of code edits. 105 | * @param useExtensions Whether extensions should be used or not. 106 | * @returns This instance for chaining. 107 | */ 108 | public useExtensions(useExtensions: boolean): GenerationToolchain 109 | { 110 | this._useExtensions = useExtensions; 111 | return this; 112 | } 113 | 114 | /** 115 | * Returns the current repository of assets. 116 | */ 117 | public get ASSETS(): AssetBundle & typeof DEFAULT_ASSETS { 118 | return this._assets; 119 | } 120 | 121 | /** 122 | * This function allows you to add assets that you can use later when generating. 123 | * @param assets The assets that should be added to the repository. 124 | */ 125 | public addAssets(assets: AssetBundle) { 126 | this._assets = { 127 | ...assets, 128 | ...this._assets 129 | }; 130 | } 131 | 132 | /** 133 | * Create a transaction to modify internal assets from PrismaClient. 134 | * @returns An {@link EditTransaction} that you can use to modify internal assets. 135 | */ 136 | public createEditTransaction(): EditTransaction { 137 | return new EditTransaction(this); 138 | } 139 | 140 | /** 141 | * Add an edit transaction to the processing queue. Transactions are processed sequentially and can't 142 | * be created while the Generation Toolchain is processing. 143 | * @param transaction The transaction that should be queued. 144 | * @returns True if the transaction has been added to the queue, false otherwise. 145 | */ 146 | public queueEditTransaction(transaction: EditTransaction): boolean { 147 | if (!this.processPromise) 148 | this.queue.push(transaction); 149 | return !this.processPromise; 150 | } 151 | 152 | /** 153 | * Start processing the transaction queue. 154 | * @returns A Promise that will resolve when processing finishes. 155 | */ 156 | public process(): Promise { 157 | if (this.processPromise) 158 | return this.processPromise; 159 | 160 | log("Prisma Util Toolchain is starting to process the transaction queue...", "\n"); 161 | this.processPromise = new Promise(async (resolve) => { 162 | const transactionRepository: { 163 | [assetPath: string]: string 164 | } = {}; 165 | const processedTransactions: string[] = []; 166 | const processedBlocksForTransactions: { 167 | [path: string]: number 168 | } = {}; 169 | 170 | const useTransactionRepository = async (requestedKey: string) => { 171 | if (!transactionRepository[requestedKey]) 172 | transactionRepository[requestedKey] = await fs.readFile(requestedKey, "utf8"); 173 | return transactionRepository[requestedKey]; 174 | }; 175 | 176 | const updateTransactionRepository = (assetPath: string, text: string) => { 177 | transactionRepository[assetPath] = text; 178 | }; 179 | 180 | while (this.queue.length > 0) { 181 | const transaction = this.queue.pop(); 182 | if (!transaction) 183 | continue; 184 | 185 | const requestedAsset = transaction?.changedAsset; 186 | if (!validString(requestedAsset)) 187 | continue; 188 | const assetPath = convertPathToLocal(requestedAsset); 189 | 190 | const blocks = this._useExtensions ? transaction.blocks.filter(block => block.ignoreExtensions) : transaction.blocks; 191 | let processedCount = 0; 192 | 193 | for (const block of blocks) { 194 | const { from, to, ammend, strategy, search } = block; 195 | let snapshot: string = ""; 196 | let text: string = ""; 197 | 198 | if (EditStrategy.REPLACE_FULL != strategy && typeof from != "string" && typeof to != "string") { 199 | snapshot = await useTransactionRepository(assetPath); 200 | 201 | text = snapshot; 202 | text = text.replace(from, (match, ...g) => { 203 | return to(match, ...g); 204 | }); 205 | } else { 206 | if(EditStrategy.REPLACE_FULL != strategy) 207 | { 208 | if (!validString(typeof from == "string" ? from : SAFE_NULL_STRING) || !validString(typeof to == "string" ? to : SAFE_NULL_STRING) || strategy == EditStrategy.NONE) 209 | continue; 210 | } 211 | 212 | snapshot = await useTransactionRepository(assetPath); 213 | 214 | if(EditStrategy.REPLACE_FULL != strategy) 215 | { 216 | if (new RegExp(escapeRegex(typeof to == "string" ? to : SAFE_NULL_STRING), "gms").test(snapshot)) 217 | continue; 218 | } 219 | 220 | const regex = new RegExp(escapeRegex(typeof from == "string" ? from : SAFE_NULL_STRING), "gms"); 221 | 222 | if(EditStrategy.REPLACE_FULL != strategy) 223 | { 224 | if (!regex.test(snapshot)) 225 | continue; 226 | } 227 | text = snapshot; 228 | 229 | switch (strategy) { 230 | case EditStrategy.REGEX: 231 | const lines = text.split("\n"); 232 | const item = lines.filter(line => regex.test(line))[0]; 233 | 234 | if (!item) 235 | continue; 236 | 237 | let index = lines.indexOf(item); 238 | 239 | if (index == -1) 240 | continue; 241 | 242 | index = index + ammend; 243 | 244 | text = `${lines.slice(0, index).join("\n")}\n${to}\n${lines.slice(index).join("\n")}`; 245 | break; 246 | case EditStrategy.JOIN: 247 | text = text.split(regex).join(`${to}${from}`); 248 | break; 249 | case EditStrategy.REPLACE: 250 | text = text.replace(regex, `${from}${to}`); 251 | break; 252 | case EditStrategy.REPLACE_FULL: 253 | text = typeof to == "function" ? `${to(text)}${text}` : text; 254 | break; 255 | } 256 | } 257 | 258 | updateTransactionRepository(assetPath, text); 259 | processedCount++; 260 | } 261 | 262 | if (processedCount > 0) 263 | processedTransactions.push(assetPath); 264 | 265 | processedBlocksForTransactions[assetPath] = processedBlocksForTransactions[assetPath] ? processedBlocksForTransactions[assetPath] + processedCount : processedCount; 266 | } 267 | 268 | for (const [file, content] of Object.entries(transactionRepository)) { 269 | await fs.writeFile(file, content); 270 | } 271 | 272 | const frequencies: { 273 | [key: string]: number 274 | } = {}; 275 | 276 | for (const transaction of processedTransactions) { 277 | frequencies[transaction] = frequencies[transaction] ? frequencies[transaction] + 1 : 1; 278 | } 279 | 280 | const blockCount = Object.values(processedBlocksForTransactions).reduce((partialSum, a) => partialSum + a, 0); 281 | 282 | log(processedTransactions.length > 0 ? `Prisma Util Toolchain has processed the following transactions: \n${[...new Set(processedTransactions)].map(key => `- ${normalizePath(key)} ${chalk.white(chalk.bold(`(${chalk.blue(frequencies[key])} ${frequencies[key] == 1 ? "transaction" : "transactions"}, ${chalk.blue(processedBlocksForTransactions[key])} ${processedBlocksForTransactions[key] == 1 ? "block" : "blocks"})`))}`).join("\n")}\nTOTAL: ${chalk.white(`${chalk.blue(processedTransactions.length)} ${processedTransactions.length == 1 ? "transaction" : "transactions"}, ${chalk.blue(blockCount)} ${blockCount == 1 ? "block" : "blocks"}`)}` : "Prisma Util Toolchain couldn't find any differences, so it didn't process any transactions."); 283 | resolve(); 284 | }); 285 | return this.processPromise; 286 | } 287 | } 288 | 289 | /** 290 | * Edit Transaction is an interface that allows you to edit a PrismaClient internal asset without worrying 291 | * about index shifting or file searching. 292 | * 293 | * To create an EditTransaction, use the {@link GenerationToolchain.createEditTransaction} function and chain the 294 | * function calls to this class, then use {@link EditTransaction.end} when you're done. 295 | */ 296 | export class EditTransaction { 297 | private generationToolchain: GenerationToolchain; 298 | private requestedAsset: string = SAFE_NULL_STRING; 299 | private transactionBlocks: EditTransactionBlock[] = []; 300 | 301 | constructor(generationToolchain: GenerationToolchain) { 302 | this.generationToolchain = generationToolchain; 303 | } 304 | 305 | /** 306 | * Returns the path to the requested asset of this transaction. 307 | */ 308 | public get changedAsset() { 309 | return this.requestedAsset; 310 | } 311 | 312 | /** 313 | * Returns the changes for this transaction. 314 | */ 315 | public get blocks() { 316 | return this.transactionBlocks; 317 | } 318 | 319 | /** 320 | * Mark this transaction as finished. This function will add the transaction to the queue for edits 321 | * and will be processed sequentially. 322 | * @returns The {@link GenerationToolchain} instance that was used for this transaction. 323 | */ 324 | public end(): GenerationToolchain { 325 | this.generationToolchain.queueEditTransaction(this); 326 | return this.generationToolchain; 327 | } 328 | 329 | /** 330 | * Change the asset being edited in this transaction. 331 | * @param assetName The asset that you want to edit. 332 | * @returns This transaction for chaining. 333 | */ 334 | public requestAsset(assetName: string): EditTransaction { 335 | this.requestedAsset = assetName; 336 | return this; 337 | } 338 | 339 | /** 340 | * Add a transaction block to this edit transaction. 341 | * 342 | * This method isn't supposed to be called manually. 343 | * 344 | * Method Flags: @Internal @NoManual 345 | * @param transactionBlock The transaction block to add. 346 | */ 347 | public pushTransactionBlock(transactionBlock: EditTransactionBlock) { 348 | this.transactionBlocks.push(transactionBlock); 349 | } 350 | 351 | /** 352 | * Create a new change for this transaction. 353 | * @returns A new transaction block. 354 | */ 355 | public createBlock(): EditTransactionBlock { 356 | return new EditTransactionBlock(this); 357 | } 358 | } 359 | 360 | /** 361 | * A transaction block handles a change in a transaction. 362 | */ 363 | export class EditTransactionBlock { 364 | private editTransaction: EditTransaction; 365 | strategy: EditStrategy = EditStrategy.NONE; 366 | from: (string | RegExp) = SAFE_NULL_STRING; 367 | to: (string | ((...groups: string[]) => string)) = SAFE_NULL_STRING; 368 | search: string = SAFE_NULL_STRING; 369 | ammend: number = 0; 370 | ignoreExtensions: boolean = false; 371 | 372 | public constructor(editTransaction: EditTransaction) { 373 | this.editTransaction = editTransaction; 374 | } 375 | 376 | /** 377 | * Change the edit strategy for this block. 378 | * @param strategy The new strategy to use. 379 | * @returns This transaction block for chaining. 380 | */ 381 | public setStrategy(strategy: EditStrategy): EditTransactionBlock { 382 | this.strategy = strategy; 383 | return this; 384 | } 385 | 386 | /** 387 | * Disable this block based on extension status. 388 | * @param ignoreExtensions Whether this block should be ran even if extensions are enabked. 389 | * @returns This transaction block for chaining. 390 | */ 391 | public setIgnoreExtensions(ignoreExtensions: boolean): EditTransactionBlock { 392 | this.ignoreExtensions = ignoreExtensions; 393 | return this; 394 | } 395 | 396 | /** 397 | * Change a line from this asset. 398 | * @param from The line to search for. 399 | * @param modifier The value that will be added to the index. 400 | * @returns This transaction block for chaining. 401 | */ 402 | public findLine(from: string | RegExp, modifier = 0): EditTransactionBlock { 403 | this.from = from; 404 | this.ammend = modifier; 405 | return this; 406 | } 407 | 408 | /** 409 | * Append content to the file. 410 | * @param to The content to add. 411 | * @returns This transaction block for chaining. 412 | */ 413 | public appendContent(to: (string | ((...groups: string[]) => string))): EditTransactionBlock { 414 | this.to = to; 415 | return this; 416 | } 417 | 418 | /** 419 | * Add search query to be used with {@link EditStrategy.REPLACE_UNSAFE}. 420 | * @param search The search query that will be used for security. 421 | * @returns This transaction block for chaining. 422 | */ 423 | public setSearch(search: string): EditTransactionBlock { 424 | this.search = search; 425 | return this; 426 | } 427 | 428 | /** 429 | * Create a new change for this transaction. 430 | * @returns A new transaction block. 431 | */ 432 | public createBlock(): EditTransactionBlock { 433 | this.editTransaction.pushTransactionBlock(this); 434 | return this.editTransaction.createBlock(); 435 | } 436 | 437 | /** 438 | * Mark this transaction as finished. This function will add the transaction to the queue for edits 439 | * and will be processed sequentially. 440 | * @returns The {@link GenerationToolchain} instance that was used for this transaction. 441 | */ 442 | public end(): GenerationToolchain { 443 | this.editTransaction.pushTransactionBlock(this); 444 | return this.editTransaction.end(); 445 | } 446 | 447 | /** 448 | * Mark this transaction block as finished. 449 | * @returns The {@link EditTransaction} that this block belongs to. 450 | */ 451 | public endBlock(): EditTransaction { 452 | this.editTransaction.pushTransactionBlock(this); 453 | return this.editTransaction; 454 | } 455 | } 456 | 457 | /** 458 | * Instance of the Project Toolchain backend implementation. 459 | * 460 | * This is the entry-point to all code generation and PrismaClient edits, as it provides an unified interface 461 | * for making changes and creating comments. 462 | */ 463 | export const GenerationToolchainInstance = new GenerationToolchain(); -------------------------------------------------------------------------------- /src/cli/lib/toolchain/middleware.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../../logger.js"; 2 | import * as fs from 'fs/promises'; 3 | import { convertPathToLocal } from "../../utils.js"; 4 | import path from "path"; 5 | import crypto from "crypto"; 6 | import chalk from "chalk"; 7 | 8 | /** 9 | * A Feature is a middleware that will be generated. 10 | */ 11 | export type Feature = { 12 | identifier: string; 13 | code: string; 14 | } 15 | /** 16 | * Packages are the scope wrappers for middlewares. 17 | */ 18 | export type Package = { 19 | identifier: string; 20 | features: Feature[] 21 | } 22 | 23 | /** 24 | * Project Toolchain Middleware API implementation. 25 | * 26 | * This class orchestrates middleware generation and hashing and provides an unified API for using the generated code. 27 | * It provides an easy way of creating middleware and correct scoping to make sure that Prisma Util users have a smooth experience. 28 | * 29 | * This API is intended for internal use only. You should not instantiate this class, but rather use the exported 30 | * instance. ({@link MiddlewareToolchainInstance}) 31 | */ 32 | class MiddlewareToolchain { 33 | private processPromise?: Promise = undefined; 34 | private packages: { 35 | [identifier: string]: Package 36 | } = {}; 37 | 38 | constructor() {} 39 | 40 | /** 41 | * Add a middleware to the current queue. 42 | * @param pack The package name. 43 | * @param name The name of this middleware. 44 | * @param code The code for this middleware. 45 | * @returns This instance for chaining. 46 | */ 47 | public defineMiddleware(pack: string, name: string, code: string) 48 | { 49 | if(this.packages[pack]) 50 | this.packages[pack].features.push({ 51 | identifier: name, 52 | code 53 | }); 54 | else 55 | this.packages[pack] = { 56 | identifier: pack, 57 | features: [{ 58 | identifier: name, 59 | code 60 | }] 61 | }; 62 | return this; 63 | } 64 | 65 | /** 66 | * Generate all of the middleware that are currently in the queue. 67 | */ 68 | public async generate() 69 | { 70 | if(this.processPromise) 71 | return this.processPromise; 72 | 73 | log("Prisma Util Toolchain is starting to process the middleware generation queue...", "\n"); 74 | 75 | this.processPromise = new Promise(async (resolve) => { 76 | const packageNames = Object.keys(this.packages); 77 | const generatedPackageCount: { 78 | [packageIdentifier: string]: number 79 | } = {}; 80 | 81 | const updateGeneratedRepository = (transaction: string) => { 82 | generatedPackageCount[transaction] = generatedPackageCount[transaction] ? generatedPackageCount[transaction] + 1 : 1; 83 | }; 84 | 85 | for(const file of (await fs.readdir(convertPathToLocal(path.join("node_modules", "prisma-util", "toolchain", "middleware")))).filter(dirent => !packageNames.includes(dirent))) 86 | { 87 | await fs.rm(convertPathToLocal(path.join("node_modules", "prisma-util", "toolchain", "middleware", file)), { recursive: true, force: true }); 88 | } 89 | for(const pack of Object.entries(this.packages)) 90 | { 91 | const [packageName, packageData] = pack; 92 | const featureNames = packageData.features.filter(feature => !["all", "default"].includes(feature.identifier)).map(feature => feature.identifier); 93 | if(featureNames.length == 0) 94 | continue; 95 | 96 | let existing = true; 97 | 98 | const packageRoot = convertPathToLocal(path.join("node_modules", "prisma-util", "toolchain", "middleware", packageName)); 99 | try { 100 | await fs.access(packageRoot); 101 | } catch (err) { 102 | existing = false; 103 | await fs.mkdir(packageRoot); 104 | } 105 | 106 | const generatedRoot = path.join(packageRoot, "generated"); 107 | const middlewareLines = 108 | featureNames.length == 1 ? 109 | `declare const ${featureNames[0]}: (prisma: PrismaClient) => (params: any, next: (args: any) => Promise) => Promise; 110 | export default ${featureNames[0]};` : 111 | `export default middlewareBase; 112 | ${featureNames.map(feature => { 113 | return `import ${feature} from "./generated/${feature}.js";\ndeclare const ${feature}: (prisma: PrismaClient) => (params: any, next: (args: any) => Promise) => Promise;`; 114 | }).join("\n")} 115 | export { ${featureNames.join(", ")} }; 116 | `; 117 | const indexDTS = 118 | `import { PrismaClient } from '@prisma/client'; 119 | declare type AvailableMiddleware = ${featureNames.map(feature => `"${feature}"`).join(" | ")}; 120 | declare type ExportedMiddlewareBase = { 121 | [middlewareName in AvailableMiddleware]: (prisma: PrismaClient) => (params: any, next: (args: any) => Promise) => Promise; 122 | } & { 123 | /** 124 | * Add all middleware defined in this folder. 125 | * 126 | * @param prisma The instance of the PrismaClient that will be modified. 127 | * @returns The instance of the PrismaClient passed as an argument. 128 | */ 129 | all: (prisma: PrismaClient) => PrismaClient; 130 | }; 131 | declare const middlewareBase: ExportedMiddlewareBase; 132 | 133 | ${middlewareLines}`; 134 | await fs.writeFile(path.join(packageRoot, "index.d.ts"), indexDTS); 135 | 136 | const indexJS = 137 | featureNames.length == 1 ? 138 | `import ${featureNames[0]} from "./generated/${featureNames[0]}.js"; 139 | export default ${featureNames[0]};` : 140 | `${featureNames.map(feature => { 141 | return `import ${feature} from "./generated/${feature}.js";` 142 | }).join("\n")} 143 | 144 | const middlewareBase = { 145 | all: (prisma) => { 146 | ${featureNames.map(feature => { 147 | return `prisma.$use(${feature}(prisma));`; 148 | }).join("\n")} 149 | return prisma; 150 | }, 151 | ${featureNames.join(", ")} 152 | }; 153 | 154 | export default middlewareBase; 155 | export { ${featureNames.join(", ")} };`; 156 | 157 | await fs.writeFile(path.join(packageRoot, "index.js"), indexJS); 158 | if(!existing) 159 | { 160 | await fs.mkdir(generatedRoot); 161 | 162 | for(const feature of packageData.features) 163 | { 164 | await fs.writeFile(`${path.join(generatedRoot, feature.identifier)}.js`, feature.code); 165 | updateGeneratedRepository(packageName); 166 | } 167 | 168 | continue; 169 | } 170 | 171 | const paths = (await fs.readdir(generatedRoot)); 172 | for(const file of paths.filter(dirent => !featureNames.includes(path.parse(dirent).name))) 173 | { 174 | await fs.rm(path.join(generatedRoot, file), { recursive: true, force: true }); 175 | } 176 | for(const { identifier, code } of packageData.features.filter(feature => paths.includes(`${feature.identifier}.js`))) 177 | { 178 | const fileBuffer = await fs.readFile(`${path.join(generatedRoot, identifier)}.js`); 179 | 180 | const currentHashSum = crypto.createHash('sha256'); 181 | currentHashSum.update(fileBuffer); 182 | const currentHex = currentHashSum.digest('hex'); 183 | 184 | const newHashSum = crypto.createHash('sha256'); 185 | newHashSum.update(code); 186 | const newHex = newHashSum.digest('hex'); 187 | 188 | if(currentHex == newHex) 189 | continue; 190 | 191 | await fs.writeFile(`${path.join(generatedRoot, identifier)}.js`, code); 192 | updateGeneratedRepository(packageName); 193 | } 194 | 195 | for(const { identifier, code } of packageData.features.filter(feature => !paths.includes(`${feature.identifier}.js`))) 196 | { 197 | await fs.writeFile(`${path.join(generatedRoot, identifier)}.js`, code); 198 | updateGeneratedRepository(packageName); 199 | } 200 | } 201 | 202 | log(Object.keys(generatedPackageCount).length > 0 ? 203 | `Prisma Util Toolchain has processed the following middleware: 204 | ${Object.entries(this.packages).map(pack => { 205 | return `${chalk.blue(pack[0])}: ${pack[1].features.map(feature => chalk.white(feature.identifier)).join(", ")}`; 206 | }).join("\n")}` : "Prisma Util Toolchain didn't generate any middleware."); 207 | resolve(); 208 | }); 209 | return this.processPromise; 210 | } 211 | } 212 | 213 | /** 214 | * Instance of the Project Toolchain Middleware API Implementation. 215 | * 216 | * This is the entry-point to all middleware creation, as it provides an unified interface for generating scoped 217 | * middleware that are easy to use. 218 | */ 219 | export const MiddlewareToolchainInstance = new MiddlewareToolchain(); -------------------------------------------------------------------------------- /src/cli/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { errorTag, prismaCLITag, warningTag, conflictTag, successTag, experimentalTag, updateTag } from "./messages.js"; 3 | import gradient from "gradient-string"; 4 | 5 | export function log(text: string, before?: string) 6 | { 7 | console.log(`${before ? before : ""}${prismaCLITag} ${chalk.gray(text)}`); 8 | } 9 | 10 | export function error(text: string, before?: string) 11 | { 12 | console.log(`${before ? before : ""}${errorTag} ${chalk.red(text)}`); 13 | } 14 | 15 | export function update(text: string, before?: string) 16 | { 17 | console.log(`${before ? before : ""}${updateTag} ${chalk.cyan(text)}`); 18 | } 19 | 20 | export function warn(text: string, before?: string) 21 | { 22 | console.log(`${before ? before : ""}${warningTag} ${chalk.yellow(text)}`); 23 | } 24 | 25 | export function success(text: string, before?: string) 26 | { 27 | console.log(`${before ? before : ""}${successTag} ${chalk.green(text)}`); 28 | } 29 | 30 | export function conflict(text: string, before?: string) 31 | { 32 | console.log(`${before ? before : ""}${conflictTag} ${chalk.magenta(text)}`); 33 | } 34 | 35 | export function experimental(text: string, before?: string) 36 | { 37 | console.log(`${before ? before : ""}${experimentalTag} ${chalk.white(text)}`); 38 | } 39 | 40 | export function interactive(text: string, before?: string) 41 | { 42 | console.log(`${before ? before : ""}${gradient.passion(text)}`); 43 | } -------------------------------------------------------------------------------- /src/cli/messages.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | /**Premade message for the general help menu. */ 3 | export const showIntro = () => { 4 | new MessageBuilder() 5 | .withHeader() 6 | .withTitle(chalk.gray("Prisma Util is an easy tool that helps with merging schema files and running utility commands.")) 7 | .withTitle(`${chalk.gray("In the background, it uses")} ${chalk.blue("npx prisma")} ${chalk.gray("to run commands, and as such the parameters are the same.")}`) 8 | .withNewLine() 9 | .withSection("Usage", [`${chalk.gray("$")} prisma-util [command]`]) 10 | .withSection("Commands", [ 11 | ` ${chalk.gray("prepare")} Initiate a simple Prisma Util configuration file.`, 12 | ` ${chalk.gray("init")} Set up Prisma for your app`, 13 | ` ${chalk.gray("generate")} Generate artifacts (e.g. Prisma Client)`, 14 | ` ${chalk.gray("db")} Manage your database schema and lifecycle`, 15 | ` ${chalk.gray("migrate")} Migrate your database`, 16 | ` ${chalk.gray("studio")} Browse your data with Prisma Studio`, 17 | ` ${chalk.gray("format")} Format your schema`, 18 | ` ${chalk.gray("schema")} Generate schemas using Prisma Util without running additional commands`, 19 | ` ${chalk.gray("upgrade")} Migrate your configuration to the latest version`, 20 | `${chalk.gray("interactive")} Run the tutorials from the official documentation in your Terminal`]) 21 | .withSection("Flags", [`${chalk.gray("--preview-feature")} Run Preview Prisma commands`]) 22 | .withSection("Examples", 23 | [ `${chalk.gray("Set up a new Prisma project")}`, `${chalk.gray("$")} prisma-util init`, "", 24 | `${chalk.gray("Generate artifacts (e.g. Prisma Client)")}`, `${chalk.gray("$")} prisma-util generate`, "", 25 | `${chalk.gray("Browse your data")}`, `${chalk.gray("$")} prisma-util studio`, "", 26 | `${chalk.gray("Create migrations from your Prisma schema, apply them to the database, generate artifacts (e.g. Prisma Client)")}`, `${chalk.gray("$")} prisma-util migrate dev`, "", 27 | `${chalk.gray("Pull the schema from an existing database, updating the Prisma schema")}`, `${chalk.gray("$")} prisma-util db pull`, "", 28 | `${chalk.gray("Push the Prisma schema state to the database")}`, `${chalk.gray("$")} prisma-util db push` 29 | ]) 30 | .show(); 31 | } 32 | /**Little utility to create nice messages. */ 33 | export default class MessageBuilder { 34 | text: string; 35 | constructor () 36 | { 37 | this.text = ""; 38 | } 39 | 40 | withHeader(header?: string) { 41 | this.text += (header ? `\n${header}\n\n` : `\n${chalk.bold(chalk.blue("Prisma Util"))}\n\n`); 42 | return this; 43 | } 44 | 45 | withTitle(title: string) { 46 | this.text += ` ${title}\n`; 47 | return this; 48 | } 49 | 50 | withSection(title: string, items: string[], c: boolean = false) 51 | { 52 | this.text += (c ? `${title}\n\n` : `${chalk.bold(chalk.blue(title))}\n\n`); 53 | items.forEach((item) => { 54 | this.text += ` ${item}\n`; 55 | }); 56 | this.text += "\n"; 57 | return this; 58 | } 59 | 60 | withNewLine() { 61 | this.text += `\n`; 62 | return this; 63 | } 64 | 65 | show() { 66 | console.log(`${this.text}`); 67 | } 68 | } 69 | 70 | export const prismaCLITag = chalk.black.bold.bgBlue(" PRISMA UTIL "); 71 | export const errorTag = chalk.black.bold.bgRed(" ERROR "); 72 | export const warningTag = chalk.black.bold.bgYellow(" WARNING "); 73 | export const conflictTag = chalk.black.bold.bgMagenta(" CONFLICT "); 74 | export const successTag = chalk.black.bold.bgGreen(" SUCCESS "); 75 | export const experimentalTag = chalk.black.bold.bgWhite(" EXPERIMENTAL "); 76 | export const updateTag = chalk.bold.black.bgCyan(" UPDATE "); -------------------------------------------------------------------------------- /src/cli/utils.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import * as path from "path"; 3 | import * as fs from "fs/promises"; 4 | import PrismaParser, { ConfigType, OptionalFeaturesArray } from "./parser.js"; 5 | import chalk from "chalk"; 6 | import { conflict, error, experimental, log, success } from './logger.js'; 7 | import { Command } from "commander"; 8 | import inquirer from "inquirer"; 9 | import { conflictTag } from "./messages.js"; 10 | import json5 from "json5"; 11 | import { pathToFileURL } from "url"; 12 | 13 | /** __dirname doesn't exist in type: module */ 14 | const __dirname = process.cwd(); 15 | 16 | /**Send commands to prisma. */ 17 | export function usePrisma(commandString: string) { 18 | return new Promise((resolve) => { 19 | // Spawn a child process running the command. 20 | const proc = child_process.spawn(`npx --yes prisma ${commandString}`, { 21 | stdio: 'inherit', 22 | shell: true 23 | }); 24 | 25 | proc.on("exit", (signal) => { 26 | // Resolve the promise on exit 27 | resolve(); 28 | }); 29 | }) 30 | } 31 | 32 | /**Use the __dirname to create a local path (resides in project root). */ 33 | export function convertPathToLocal(p: string) { 34 | return path.resolve(__dirname, p); 35 | } 36 | 37 | /** Sub command helper */ 38 | export function createSubCommand(command: Command, nameAndArgs: string) { 39 | const subCommand = command.command(nameAndArgs); 40 | return subCommand; 41 | } 42 | 43 | /** Used to run commands with timings. */ 44 | export async function runPrismaCommand(command: string) 45 | { 46 | // Measure execution time 47 | const start = process.hrtime(); 48 | log(`${chalk.gray(`prisma ${command}`)}\n`, "\n") 49 | // Run Prisma command and pipe io 50 | const output = await usePrisma(command); 51 | const elapsed = process.hrtime(start)[1] / 1000000; 52 | // Print execution time and exit 53 | log(`${chalk.gray("Command executed in")} ${chalk.blue(process.hrtime(start)[0] + "s ")}${chalk.gray("and ")}${chalk.blue(elapsed.toFixed(2) + "ms")}${chalk.gray(".")}`, "\n"); 54 | return output; 55 | } 56 | 57 | /** 58 | * Normalize path according to the project root. 59 | * @param path The path to normalize. 60 | */ 61 | export function normalizePath(p: string) { 62 | return path.relative(__dirname, p); 63 | } 64 | 65 | /** 66 | * Get configuration path. 67 | */ 68 | export async function getConfigurationPath(configPath: string) 69 | { 70 | const packConfig = JSON.parse(await fs.readFile(convertPathToLocal("./package.json"), "utf8")); 71 | const folder = packConfig.prismaUtil ? packConfig.prismaUtil : "prisma-util"; 72 | configPath = configPath == "" ? (packConfig.prismaUtilConfig ? packConfig.prismaUtilConfig : "config.mjs") : configPath; 73 | const p = convertPathToLocal(path.join(folder, configPath)); 74 | 75 | return p; 76 | } 77 | 78 | /** 79 | * Utility function to Walk a directory. 80 | * @param directory The directory to search. 81 | * @returns A flattened array of paths. 82 | */ 83 | export async function getFiles(directory: string): Promise { 84 | const dirents = await fs.readdir(directory, { withFileTypes: true }); 85 | const files = await Promise.all(dirents.map((dirent) => { 86 | const res = path.join(directory, dirent.name); 87 | return dirent.isDirectory() ? getFiles(res) : res; 88 | })); 89 | return Array.prototype.concat(...files); 90 | } 91 | 92 | /** Create or read the config. */ 93 | export async function createConfig(configPath: string) { 94 | const packConfig = JSON.parse(await fs.readFile(convertPathToLocal("./package.json"), "utf8")); 95 | const folder = packConfig.prismaUtil ? packConfig.prismaUtil : "prisma-util"; 96 | configPath = configPath == "" ? (packConfig.prismaUtilConfig ? packConfig.prismaUtilConfig : "config.mjs") : configPath; 97 | const p = convertPathToLocal(path.join(folder, configPath)); 98 | let created = false; 99 | try { 100 | await fs.access(p, fs.constants.R_OK); 101 | } catch(_) 102 | { 103 | created = true; 104 | } 105 | let json: ConfigType = { 106 | optionalFeatures: [], 107 | includeFiles: [], 108 | baseSchema: "", 109 | toolchain: { 110 | useExtensions: false, 111 | resolve: { 112 | types: "./types" 113 | } 114 | } 115 | } 116 | let textToWrite = ""; 117 | try { 118 | textToWrite = (await fs.readFile(p, "utf8")).replace(/@typedef {".*} OptionalFeatures/gms, `@typedef {${OptionalFeaturesArray.map(feature => `"${feature}"`).join(" | ")}} OptionalFeatures`); 119 | json = (await import(pathToFileURL(p).toString())).default; 120 | if(configPath == "" && (!packConfig.prismaUtilConfig || !packConfig.prismaUtil)) 121 | { 122 | packConfig.prismaUtil = folder; 123 | packConfig.prismaUtilConfig = configPath; 124 | await fs.writeFile(convertPathToLocal("package.json"), JSON.stringify(packConfig, null, 2)); 125 | } 126 | } catch (err) { 127 | if(created) 128 | { 129 | textToWrite = 130 | `// @ts-check 131 | 132 | /** 133 | * @typedef {string | ((generator?: any) => string)} FileGeneratorConfig 134 | * @typedef {string | ((model?: any, name?: any) => string)} FileModelConfig 135 | * @typedef {${OptionalFeaturesArray.map(feature => `"${feature}"`).join(" | ")}} OptionalFeatures 136 | */ 137 | 138 | /** 139 | * @typedef {Object} IntrospectionModel 140 | * 141 | * @property {String} name 142 | * The name of this model. If this parameter hasn't been modified before, it will be the table name from the database. 143 | * 144 | * @property {(attribute: string) => void} addAttribute 145 | * Add an attribute to this model. 146 | * 147 | * attribute - The attribute to add. You can use the \`schema-creator\` module for a list of attributes. 148 | */ 149 | 150 | /** 151 | * @typedef {Object} ResolveConfiguration 152 | * 153 | * @property {String} types 154 | * Path to the types folder relative to the folder specified in \`package.json\`. 155 | * To find out more about configuring the types folder, read {@link https://prisma-util.gitbook.io/prisma-util/modules/project-toolchain/api-documentation#types this} documentation section. 156 | */ 157 | 158 | /** 159 | * @typedef {Object} ProjectToolchainConfiguration 160 | * 161 | * @property {boolean} useExtensions 162 | * Whether Project Toolchain should use client extensions or middleware. 163 | * To find out more about configuring extension usage, read {@link https://prisma-util.gitbook.io/prisma-util/modules/project-toolchain/api-documentation#use-extensions this} documentation section. 164 | * 165 | * @property {ResolveConfiguration} resolve 166 | * Help Project Toolchain resolve your assets correctly. 167 | * To find out more about configuring resolve roots, read {@link https://prisma-util.gitbook.io/prisma-util/modules/project-toolchain/api-documentation#resolve this} documentation section. 168 | * 169 | */ 170 | 171 | /** 172 | * @typedef {Object} Configuration 173 | * 174 | * @property {FileModelConfig} baseSchema 175 | * The file that contains your generator and datasource. This path is relative to your project root. 176 | * To find out more about configuring the base schema, read {@link https://prisma-util.gitbook.io/prisma-util/api-documentation/configuration-reference/base-schema this} documentation section. 177 | * 178 | * @property {FileModelConfig[]} includeFiles 179 | * Files in this array will be merged in to the final schema by Prisma Util. 180 | * To find out more about configuring the included files, read {@link https://prisma-util.gitbook.io/prisma-util/api-documentation/configuration-reference/include-files this} documentation section. 181 | * 182 | * @property {string[]?} [excludeModels] 183 | * This array uses the \`file:model\` association defined in the Prisma Util concepts. Models in this array will be excluded from the final build. 184 | * To find out more about configuring the excluded models, read {@link https://prisma-util.gitbook.io/prisma-util/api-documentation/configuration-reference/exclude-models this} documentation section. 185 | * 186 | * @property {OptionalFeatures[]} optionalFeatures 187 | * Allows you to enable optional features to supercharge your Prisma Util setup. 188 | * To find out more about configuring optional features, read {@link https://prisma-util.gitbook.io/prisma-util/api-documentation/configuration-reference/optional-features this} documentation section. 189 | * 190 | * @property {{[fileModel: string]: string}?} [extended] 191 | * Create model inheritance within Prisma! The model defined by the value of this key-value entry will receive all non-id non-relation fields from the model defined by the key. 192 | * To find out more about configuring model inheritance, read {@link https://prisma-util.gitbook.io/prisma-util/api-documentation/configuration-reference/extend-models this} documentation section. 193 | * 194 | * @property {ProjectToolchainConfiguration} toolchain 195 | * Project toolchain configuration block. 196 | * To find out more about configuring Project Toolchain, read {@link https://prisma-util.gitbook.io/prisma-util/api-documentation/configuration-reference/toolchain this} documentation section. 197 | */ 198 | 199 | /** 200 | * @type {Configuration} 201 | */ 202 | export default ${json5.stringify(json, null, 4)};`; 203 | try { 204 | await fs.mkdir(convertPathToLocal("prisma-util")); 205 | await fs.mkdir(convertPathToLocal(path.join("prisma-util", "types"))); 206 | await fs.mkdir(convertPathToLocal(path.join("prisma-util", "functions"))); 207 | if(!packConfig.prismaUtil) 208 | { 209 | packConfig.prismaUtil = "prisma-util"; 210 | await fs.writeFile(convertPathToLocal("package.json"), JSON.stringify(packConfig, null, 2)); 211 | } 212 | if(!packConfig.prismaUtilConfig) 213 | { 214 | packConfig.prismaUtilConfig = configPath; 215 | } 216 | } catch (e) { 217 | } 218 | } else 219 | { 220 | error("The configuration file is invalid.", "\n"); 221 | process.exit(1); 222 | } 223 | } 224 | await fs.writeFile(p, textToWrite); 225 | return { 226 | configData: json, 227 | created 228 | }; 229 | } 230 | 231 | const regex = /(?:\{(?:<(.+?\.json)>)\.(.+?)\})/gms; 232 | /**Use data from json files inside configuration. */ 233 | export async function matchJSON(s: string) 234 | { 235 | const fileContentMap: { 236 | [file: string]: any 237 | } = {}; 238 | for(let results; results = regex.exec(s);) 239 | { 240 | const file = results[1]; 241 | if(!fileContentMap[file]) 242 | fileContentMap[file] = JSON.parse(await fs.readFile(convertPathToLocal(file), "utf8")); 243 | } 244 | return s.replace(regex, (match, file, p2) => { 245 | const path = p2.split("."); 246 | let object = fileContentMap[file]; 247 | for(const p of path) 248 | object = object[p]; 249 | return object; 250 | }); 251 | } 252 | 253 | /**Load a .prisma file from the config. */ 254 | export async function getSchema(path: string) { 255 | try { 256 | return await fs.readFile(convertPathToLocal(path), "utf-8"); 257 | } catch (err) { 258 | error(`The ${chalk.bold(path)} schema file doesn't exist!`); 259 | process.exit(1); 260 | } 261 | } 262 | 263 | /** Flatten array of arrays. */ 264 | export function flatten(array: any[][]): any[] { 265 | return array.reduce(function (flatArray, arrayToFlatten) { 266 | return flatArray.concat(Array.isArray(arrayToFlatten) ? flatten(arrayToFlatten) : arrayToFlatten); 267 | }, []); 268 | } 269 | 270 | /** Ends with any string in array. */ 271 | export function endsWithAny(item: string, array: string[]) 272 | { 273 | let returnValue = null; 274 | for(const test of array) { 275 | if(item.toLowerCase().endsWith(test.toLowerCase())) 276 | { 277 | returnValue = test; 278 | break; 279 | } 280 | }; 281 | return returnValue; 282 | } 283 | 284 | /**Write temp file so prisma can read it. */ 285 | export async function writeTempSchema(content: string, path?: string) 286 | { 287 | try { 288 | await fs.writeFile(convertPathToLocal(path ? path : "./node_modules/.bin/generated-schema.prisma"), content); 289 | } catch(err) { 290 | error("An error has occured while writing the generated schema."); 291 | console.error(err); 292 | process.exit(1); 293 | } 294 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import * as fs from 'fs/promises'; 3 | import { convertPathToLocal } from "./utils.js"; 4 | import { pathToFileURL } from "url"; 5 | import generated from "./generated.js"; 6 | 7 | export function getConfig() 8 | { 9 | return generated as any; 10 | } -------------------------------------------------------------------------------- /src/lib/functions.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "./config.js"; 2 | 3 | export default function getMappings() 4 | { 5 | const config = getConfig(); 6 | 7 | if(!config.defaultFunctions) 8 | return { columnMappings: {}, modelMappings: {} }; 9 | 10 | const entries = Object.entries(config.defaultFunctions).map(entry => { 11 | return [entry[0].split(":")[1], entry[1]] as [string, Function]; 12 | }); 13 | 14 | const columnMappings = Object.fromEntries(entries); 15 | 16 | const modelMappings: { 17 | [model: string]: { 18 | [column: string]: Function 19 | } 20 | } = {}; 21 | 22 | for(const [modelColumn, func] of entries) 23 | { 24 | const [modelName, columnName] = modelColumn.split("."); 25 | if(modelMappings[modelName]) 26 | modelMappings[modelName][columnName] = func; 27 | else 28 | modelMappings[modelName] = { [columnName]: func }; 29 | } 30 | 31 | return { columnMappings, modelMappings }; 32 | } 33 | 34 | export function getStaticTake() 35 | { 36 | const config = getConfig(); 37 | if(!config.take) 38 | return {}; 39 | return config.take; 40 | } -------------------------------------------------------------------------------- /src/lib/generated.ts: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | export function getRealDirectory() 5 | { 6 | const resolvedDir = path.resolve('.') 7 | const realDir = fs.realpathSync.native(resolvedDir) 8 | return realDir; 9 | } 10 | 11 | export function convertPathToLocal(p: string) 12 | { 13 | return path.join(getRealDirectory(), p); 14 | } -------------------------------------------------------------------------------- /src/schema-creator/creator.ts: -------------------------------------------------------------------------------- 1 | import Enum from "./enum.js"; 2 | import Model from "./model.js"; 3 | 4 | /** Abstract Creator that Model and Enum extend. */ 5 | export default abstract class AbstractCreator { 6 | /** Create a new model. */ 7 | abstract model(name: string): Model; 8 | /** Create a new enum. */ 9 | abstract enum(name: string): Enum; 10 | /** Build the schema into a string that can be parsed. */ 11 | abstract build(): string; 12 | } -------------------------------------------------------------------------------- /src/schema-creator/enum.ts: -------------------------------------------------------------------------------- 1 | import AbstractCreator from "./creator.js"; 2 | import { SchemaCreator } from "./index.js"; 3 | import Model from "./model.js"; 4 | 5 | /** Enum that will be created in your Prisma schema. 6 | * 7 | * When resolving conflicts, this enum will be displayed as `codeSchemas:[EnumName]` so you can differentiate between .schema files and code generated models. 8 | * 9 | * For additional functionality, you can use the same format (`codeSchemas:[ModelName].[columnName]`) to remap columns using the Automatic Remapper. 10 | */ 11 | export default class Enum extends AbstractCreator { 12 | /** Reference to creator for handling chaining. */ 13 | private creator: SchemaCreator; 14 | /** Enum name. */ 15 | _name: string; 16 | /** List of items. */ 17 | items: string[]; 18 | constructor(creator: SchemaCreator, name: string) 19 | { 20 | super(); 21 | this.creator = creator; 22 | this._name = name; 23 | this.items = []; 24 | } 25 | 26 | /** Change this enum's name. */ 27 | name(name: string) { 28 | this._name = name; 29 | return this; 30 | } 31 | 32 | /** Add an enum item. */ 33 | item(name: string) 34 | { 35 | this.items.push(name); 36 | return this; 37 | } 38 | 39 | model(name: string): Model { 40 | return this.creator.pushEnum(this).model(name); 41 | } 42 | 43 | enum(name: string): Enum { 44 | return this.creator.pushEnum(this).enum(name); 45 | } 46 | 47 | /** You should not call this method yourself. */ 48 | beforeBuild() { 49 | return this; 50 | } 51 | 52 | build(): string { 53 | return this.creator.pushEnum(this).build(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/schema-creator/index.ts: -------------------------------------------------------------------------------- 1 | import AbstractCreator from "./creator.js"; 2 | import Enum from "./enum.js"; 3 | import Model from "./model.js"; 4 | import glob from "glob"; 5 | import * as child_process from 'child_process'; 6 | import * as fs from 'fs/promises'; 7 | import { matchJSON } from "../cli/utils.js"; 8 | import path from "path"; 9 | import { pathToFileURL } from "url"; 10 | 11 | /** Allows schema creation via code. */ 12 | export class SchemaCreator extends AbstractCreator { 13 | /** Models that will be generated */ 14 | private models: Model[]; 15 | /** Enums that will be generated */ 16 | private enums: Enum[]; 17 | 18 | /** Singleton pattern. */ 19 | private static instance: SchemaCreator; 20 | 21 | private constructor() { 22 | super(); 23 | this.models = []; 24 | this.enums = []; 25 | } 26 | 27 | /** Internal method for assigning creators. */ 28 | private static getInstance() 29 | { 30 | if (!SchemaCreator.instance) { 31 | SchemaCreator.instance = new SchemaCreator(); 32 | } 33 | 34 | return SchemaCreator.instance; 35 | } 36 | 37 | /** Method to push models. You should not use this manually, as it's handled internally. */ 38 | pushModel(model: Model) { 39 | this.models.push(model); 40 | return this; 41 | } 42 | 43 | /** Method to push enums. You should not use this manually, as it's handled internally. */ 44 | pushEnum(e: Enum) { 45 | this.enums.push(e); 46 | return this; 47 | } 48 | 49 | /** Create a new model. */ 50 | static model(name: string) 51 | { 52 | return new Model(this.getInstance(), name); 53 | } 54 | 55 | /** Create a new enum. */ 56 | static enum(name: string) 57 | { 58 | return new Enum(this.getInstance(), name); 59 | } 60 | 61 | /** Build the schema into a string that can be parsed. */ 62 | static build() 63 | { 64 | return this.getInstance().build(); 65 | } 66 | 67 | model(name: string): Model { 68 | return new Model(this, name); 69 | } 70 | 71 | enum(name: string): Enum { 72 | return new Enum(this, name); 73 | } 74 | 75 | build(): string { 76 | let schema = ""; 77 | for(let model of this.models) 78 | { 79 | model = model.beforeBuild(); 80 | let columns = model.columns.map(column => ` ${column.name} ${column.type} ${column.constraints.join(" ")}`).join("\n"); 81 | schema = `${schema}\n\nmodel ${model._name} {\n${columns}\n}`; 82 | } 83 | for(let en of this.enums) 84 | { 85 | en = en.beforeBuild(); 86 | schema = `${schema}\n\nenum ${en._name} {\n ${en.items.join("\n ")}\n}`; 87 | } 88 | return schema; 89 | } 90 | } 91 | 92 | /** Utility for including files using glob. 93 | * Will always return an array of {@link https://prisma-util.gitbook.io/prisma-util/api-documentation#path Path}s. 94 | */ 95 | export function globModels(base: (((model?: string, column?: string) => string) | string), globPattern = "**/*.prisma") 96 | { 97 | const baseSchema = typeof base == "function" ? base() : base; 98 | return glob.sync(globPattern, { 99 | ignore: "node_modules/**/*" 100 | }).filter(path => path != baseSchema); 101 | } 102 | 103 | /** 104 | * Utility for running commands in migration hooks. 105 | * This function will return a Promise that resolves when the command has finished execution. 106 | */ 107 | export function execCommand(command: string) 108 | { 109 | return new Promise((resolve) => { 110 | // Spawn a child process running the command. 111 | const proc = child_process.spawn(command, { 112 | stdio: 'inherit', 113 | shell: true 114 | }); 115 | 116 | proc.on("exit", (signal) => { 117 | // Resolve the promise on exit 118 | resolve(); 119 | }); 120 | }) 121 | } 122 | 123 | /** 124 | * Utility for defining a function folder. 125 | * 126 | * This function will return an array of functions. 127 | */ 128 | export async function functionsFolder(folderPath: string) 129 | { 130 | const p = await matchJSON(folderPath); 131 | 132 | const functions: { 133 | [name: string]: Function 134 | } = {}; 135 | const files = (await fs.readdir(p)); 136 | for(const file of files) 137 | { 138 | const entries = Object.entries((await import(pathToFileURL(path.join(p, file)).toString()))); 139 | for(const entry of entries) 140 | { 141 | const [name, func] = entry as [string, Function]; 142 | if(!functions[name]) 143 | functions[name] = func; 144 | } 145 | } 146 | 147 | return Object.values(functions); 148 | } 149 | 150 | import * as dotenv from "dotenv"; 151 | 152 | /** 153 | * Import an .env file to the Virtual Environment. 154 | * @param path Path relative to your project root pointing to the env file that needs to be imported. 155 | */ 156 | export function useEnv(path: string) 157 | { 158 | dotenv.config({ path: path, override: true }); 159 | } 160 | 161 | /** Utility for easier file:model.column associations. */ 162 | export function constantModel(path: string) { 163 | /** 164 | * Utility for easier file:model.column associations. 165 | * 166 | * Not providing a model will return a {@link https://prisma-util.gitbook.io/prisma-util/api-documentation#path Path}. 167 | * 168 | * Providing a model will return a {@link https://prisma-util.gitbook.io/prisma-util/api-documentation#file-model FileModel}. 169 | * 170 | * Providing both a model and a column will return a {@link https://prisma-util.gitbook.io/prisma-util/api-documentation#file-model-column FileModelColumn}. 171 | */ 172 | return function(model?: string, column?: string) 173 | { 174 | if(model) 175 | { 176 | if(column) 177 | { 178 | return `${path}:${model}.${column}`; 179 | } 180 | return `${path}:${model}`; 181 | } 182 | return path; 183 | } 184 | } 185 | 186 | /** Utility for easier file:generator associations. */ 187 | export function constantGenerator(path: string) { 188 | /** 189 | * Utility for easier file:generator associations. 190 | * 191 | * Not providing a generator will return a {@link https://prisma-util.gitbook.io/prisma-util/api-documentation#path Path}. 192 | * 193 | * Providing a generator will return a {@link https://prisma-util.gitbook.io/prisma-util/api-documentation#file-generator FileGenerator}. 194 | * 195 | */ 196 | return function(generator?: string) 197 | { 198 | if(generator) 199 | { 200 | return `${path}:${generator}`; 201 | } 202 | return path; 203 | } 204 | } 205 | 206 | /** Utility for importing types inside of JavaScript. */ 207 | export function importType(path: string, typeName: string) { 208 | return `${path}:${typeName}`; 209 | } 210 | 211 | /** Utility function for accessing environment variables. */ 212 | export function env(variable: string, def?: string): string { 213 | let returnValue = process.env[variable]; 214 | if(returnValue) 215 | return returnValue; 216 | return def ? def : ""; 217 | } 218 | 219 | type ReferentialAction = "Cascade" | "NoAction" | "Restrict" | "SetDefault" | "SetNull"; 220 | 221 | /** Constraints that you can add to your columns and models. */ 222 | export const Constraints = { 223 | /** These constraints can be used anywhere. */ 224 | DB: (method: string, ...args: string[]) => `@db.${method}${args.length > 0 ? `(${args.join(", ")})` : ""}`, 225 | /** These constraints can only be applied to columns. */ 226 | Column: { 227 | /** Defines a single-field ID on the model. */ 228 | ID: (args?: { 229 | map?: string, length?: number, sort?: string, clustered?: boolean 230 | }) => args ? `@id(${Object.entries(args).map(pair => `${pair[0]}: ${typeof pair[1] == "string" ? `"${pair[1]}"` : pair[1]}`).join(", ")})` : "@id", 231 | /** Defines a default value for a field. */ 232 | DEFAULT: (value: string, args?: { 233 | map?: string 234 | }) => args ? `@default(${value}${args ? `, map: "${args.map}"` : ""})` : `@default(${value})`, 235 | /** Defines a unique constraint for this field. */ 236 | UNIQUE: (args?: { 237 | map?: string, length?: number, sort?: string, clustered?: boolean 238 | }) => args ? `@unique(${Object.entries(args).map(pair => `${pair[0]}: ${typeof pair[1] == "string" ? `"${pair[1]}"` : pair[1]}`).join(", ")})` : "@unique", 239 | /** Defines meta information about the relation */ 240 | RELATION: (args: { 241 | name?: string, 242 | fields: string[], 243 | references: string[], onDelete?: ReferentialAction, onUpdate?: ReferentialAction, map?: string 244 | }) => `@relation(${Object.entries(args).map(pair => `${pair[0]}: ${typeof pair[1] == "string" ? `"${pair[1]}"` : `[${pair[1].join(", ")}]`}`).join(", ")})`, 245 | /** Maps a field name or enum value from the Prisma schema to a column or document field with a different name in the database. If you do not use @map, the Prisma field name matches the column name or document field name exactly. */ 246 | MAP: (name: string) => `@map("${name}")`, 247 | /** Automatically stores the time when a record was last updated. If you do not supply a time yourself, the Prisma Client will automatically set the value for fields with this attribute. */ 248 | UPDATEDAT: () => "@updatedAt", 249 | /** In 2.17.0 and later, Prisma adds @ignore to fields that refer to invalid models when you introspect. */ 250 | IGNORE: () => "@ignore" 251 | }, 252 | /** These constraints can only be applied to models. */ 253 | Model: { 254 | /** Defines a multi-field ID on the model. */ 255 | ID: (fields: string[], args?: { 256 | map?: string, length?: number, sort?: string, clustered?: boolean 257 | }) => `@@id(fields: [${fields.join(", ")}]${args ? `${Object.entries(args).length > 0 ? ", " : ""}${Object.entries(args).map(pair => `${pair[0]}: ${typeof pair[1] == "string" ? `"${pair[1]}"` : pair[1]}`).join(", ")}` : ""})`, 258 | /** Defines a compound unique constraint for the specified fields. */ 259 | UNIQUE: (fields: string[], args?: { 260 | map?: string, length?: number, sort?: string, clustered?: boolean 261 | }) => `@@unique(fields: [${fields.join(", ")}]${args ? `${Object.entries(args).length > 0 ? ", " : ""}${Object.entries(args).map(pair => `${pair[0]}: ${typeof pair[1] == "string" ? `"${pair[1]}"` : pair[1]}`).join(", ")}` : ""})`, 262 | /** Defines an index in the database. */ 263 | INDEX: (fields: string[], args?: { 264 | name?: string, type?: string, map?: string, length?: number, sort?: string, clustered?: boolean, ops?: string 265 | }) => `@@index(fields: [${fields.join(", ")}]${args ? `${Object.entries(args).length > 0 ? ", " : ""}${Object.entries(args).map(pair => `${pair[0]}: ${typeof pair[1] == "string" ? `"${pair[1]}"` : pair[1]}`).join(", ")}` : ""})`, 266 | /** Maps the Prisma schema model name to a table (relational databases) or collection (MongoDB) with a different name, or an enum name to a different underlying enum in the database. If you do not use @@map, the model name matches the table (relational databases) or collection (MongoDB) name exactly. */ 267 | MAP: (name: string) => `@@map("${name}")`, 268 | /** In 2.17.0 and later, Prisma adds @@ignore to an invalid model instead of commenting it out. */ 269 | IGNORE: () => "@@ignore" 270 | } 271 | }; 272 | 273 | /** Functions that you can use in your constraints. */ 274 | export const Functions = { 275 | /** Represents default values that are automatically generated by the database. */ 276 | AUTO: () => "auto()", 277 | /** Create a sequence of integers in the underlying database and assign the incremented values to the ID values of the created records based on the sequence. */ 278 | AUTOINCREMENT: () => "autoincrement()", 279 | /** Create a sequence of integers in the underlying database and assign the incremented values to the values of the created records based on the sequence. */ 280 | SEQUENCE: (argument?: "virtual" | {cache: number} | {increment: number} | {minValue: number} | {maxValue: number} | {start: number}) => argument ? `sequence(${argument == "virtual" ? "virtual": `${Object.keys(argument)[0]}: ${Object.values(argument)[0]}`})` : "sequence()", 281 | /** Generate a globally unique identifier based on the cuid spec.*/ 282 | CUID: () => "cuid()", 283 | /** Generate a globally unique identifier based on the UUID spec.*/ 284 | UUID: () => "uuid()", 285 | /** Set a timestamp of the time when a record is created. */ 286 | NOW: () => "now()", 287 | /**Represents default values that cannot be expressed in the Prisma schema (such as random()). */ 288 | DBGENERATED: (argument?: string) => `dbgenerated(${argument ? `"${argument}"` : ""})` 289 | }; -------------------------------------------------------------------------------- /src/schema-creator/model.ts: -------------------------------------------------------------------------------- 1 | import AbstractCreator from './creator.js'; 2 | import Enum from './enum.js'; 3 | import { SchemaCreator } from './index.js'; 4 | 5 | /** Hidden column class for internal use. */ 6 | class Column { 7 | /** Column name. */ 8 | readonly name: string; 9 | /** Column type. */ 10 | readonly type: string; 11 | /** Column constraints. */ 12 | readonly constraints: string[]; 13 | constructor(name: string, type: string, ...constraints: string[]) 14 | { 15 | this.name = name; 16 | this.type = type; 17 | this.constraints = constraints; 18 | } 19 | } 20 | /** Model that will be created in your Prisma schema. 21 | * 22 | * When resolving conflicts, this model will be displayed as `codeSchemas:[ModelName]` so you can differentiate between .schema files and code generated models. 23 | * 24 | * For additional functionality, you can use the same format (`codeSchemas:[ModelName].[columnName]`) to remap columns using the Automatic Remapper. 25 | */ 26 | export default class Model extends AbstractCreator { 27 | /** Reference to creator for handling chaining. */ 28 | private creator: SchemaCreator; 29 | /** Model name. */ 30 | _name: string; 31 | /** List of columns. */ 32 | columns: Column[]; 33 | /** List of model attributes. */ 34 | attributes: string[]; 35 | constructor(creator: SchemaCreator, name: string) 36 | { 37 | super(); 38 | this.creator = creator; 39 | this._name = name; 40 | this.attributes = []; 41 | this.columns = []; 42 | } 43 | 44 | /** Change this model's name. */ 45 | name(name: string) { 46 | this._name = name; 47 | return this; 48 | } 49 | 50 | /** Create a new column. */ 51 | column(name: string, type: string, ...constraints: string[]) 52 | { 53 | this.columns.push(new Column(name, type, ...constraints)); 54 | return this; 55 | } 56 | 57 | /**Add constraints to this model. */ 58 | constraints(...constraints: string[]) 59 | { 60 | this.attributes.push(...constraints); 61 | return this; 62 | } 63 | 64 | model(name: string): Model { 65 | return this.creator.pushModel(this).model(name); 66 | } 67 | 68 | enum(name: string): Enum { 69 | return this.creator.pushModel(this).enum(name); 70 | } 71 | 72 | /** You should not call this method yourself. */ 73 | beforeBuild() { 74 | if(this.attributes.length == 0) 75 | return this; 76 | while(this.columns.filter(column => column.type == "").length > 0) 77 | this.columns.splice(this.columns.indexOf(this.columns.filter(column => column.type == "")[0]), 1) 78 | 79 | for(const attr of this.attributes) 80 | this.column(attr, ""); 81 | return this; 82 | } 83 | 84 | build(): string { 85 | return this.creator.pushModel(this).build(); 86 | } 87 | } -------------------------------------------------------------------------------- /src/toolchain/extensions/index.ts: -------------------------------------------------------------------------------- 1 | export {}; -------------------------------------------------------------------------------- /src/toolchain/index.ts: -------------------------------------------------------------------------------- 1 | // IMPORTS WILL BE GENERATED BELOW THIS LINE 2 | 3 | const middleware: { 4 | [key: string]: (prisma: any) => { 5 | params: any, 6 | next: (params: any) => Promise 7 | } 8 | } = { 9 | // MIDDLEWARE WILL BE GENERATED BELOW THIS LINE 10 | }; 11 | 12 | export default middleware; -------------------------------------------------------------------------------- /src/toolchain/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export {}; -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | export const LIB_VERSION = "0.0.0-development"; 2 | -------------------------------------------------------------------------------- /tools/copy-distribution-l.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, copyFileSync, readFileSync } from "fs"; 2 | import { resolve, join, relative, dirname } from "path"; 3 | 4 | const _dirname = process.cwd(); 5 | 6 | const destination = resolve(_dirname, "build"); 7 | const files = ["README.MD", "LICENSE", "package.json"]; 8 | const removedProperties = ["scripts"]; 9 | 10 | for(const file of files) 11 | { 12 | 13 | if(file == "package.json") 14 | { 15 | const packageJSON = JSON.parse(readFileSync(resolve(_dirname, "package.json"), "utf8")); 16 | for(const property of removedProperties) 17 | delete packageJSON[property]; 18 | writeFileSync(join(destination, file), JSON.stringify(packageJSON, null, 4)); 19 | } else 20 | { 21 | copyFileSync(resolve(_dirname, file), resolve(destination, file)); 22 | } 23 | } -------------------------------------------------------------------------------- /tools/copy-distribution.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, copyFileSync, readFileSync } from "fs"; 2 | import { resolve, join, relative, dirname } from "path"; 3 | 4 | const _dirname = join(process.cwd(), ".."); 5 | 6 | const destination = resolve(_dirname, "build"); 7 | const files = ["README.MD", "LICENSE"]; 8 | const removedProperties = ["scripts"]; 9 | 10 | for(const file of files) 11 | { 12 | /* 13 | * 14 | * if(file == "package.json") 15 | * { 16 | * const packageJSON = JSON.parse(readFileSync(resolve(_dirname, "package.json"), "utf8")); 17 | * for(const property of removedProperties) 18 | * delete packageJSON[property]; 19 | * writeFileSync(join(destination, file), JSON.stringify(packageJSON, null, 4)); 20 | * } else 21 | * { 22 | */ 23 | copyFileSync(resolve(_dirname, file), resolve(destination, file)); 24 | } -------------------------------------------------------------------------------- /tools/set-env.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import path from "path"; 3 | 4 | const __dirname = process.cwd(); 5 | const pathToUse = path.join(__dirname, "build", "cli", "index.js"); 6 | 7 | const current = readFileSync(pathToUse, "utf8").split("\n"); 8 | writeFileSync(pathToUse, 9 | `${current.slice(0, 1).join("\n")} 10 | process.env.ENV = "dev"; 11 | ${current.slice(1).join("\n")}` 12 | ); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "ESNext", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "outDir": "build", 8 | "rootDir": "src", 9 | "strict": true, 10 | "types": ["node"], 11 | "esModuleInterop": true, 12 | "moduleResolution": "node" 13 | } 14 | } --------------------------------------------------------------------------------