├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .gitpod.yml ├── .husky ├── pre-commit └── pre-push ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin └── cli.js ├── examples ├── .gitignore ├── README.md ├── package.json ├── prisma │ └── schema.prisma ├── script.ts ├── tsconfig.json └── yarn.lock ├── images └── zod-prisma.svg ├── package.json ├── src ├── config.ts ├── docs.ts ├── generator.ts ├── index.ts ├── test │ ├── docs.test.ts │ ├── functional │ │ ├── basic │ │ │ ├── expected │ │ │ │ ├── document.ts │ │ │ │ ├── index.ts │ │ │ │ ├── presentation.ts │ │ │ │ └── spreadsheet.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── config-import │ │ │ ├── expected │ │ │ │ ├── document.ts │ │ │ │ └── index.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ ├── schema.prisma │ │ │ │ └── zod-utils.ts │ │ ├── config │ │ │ ├── expected │ │ │ │ ├── index.ts │ │ │ │ ├── post.ts │ │ │ │ └── user.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── different-client-path │ │ │ ├── expected │ │ │ │ ├── document.ts │ │ │ │ └── index.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── docs │ │ │ ├── expected │ │ │ │ ├── index.ts │ │ │ │ └── post.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── driver.test.ts │ │ ├── imports │ │ │ ├── expected │ │ │ │ ├── document.ts │ │ │ │ ├── index.ts │ │ │ │ ├── presentation.ts │ │ │ │ └── spreadsheet.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── json │ │ │ ├── expected │ │ │ │ ├── index.ts │ │ │ │ ├── post.ts │ │ │ │ └── user.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── optional │ │ │ ├── expected │ │ │ │ ├── index.ts │ │ │ │ ├── post.ts │ │ │ │ └── user.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── recursive │ │ │ ├── expected │ │ │ │ ├── comment.ts │ │ │ │ └── index.ts │ │ │ └── prisma │ │ │ │ ├── .client │ │ │ │ └── index.ts │ │ │ │ └── schema.prisma │ │ ├── relation-1to1 │ │ │ ├── expected │ │ │ │ ├── index.ts │ │ │ │ ├── keychain.ts │ │ │ │ └── user.ts │ │ │ └── prisma │ │ │ │ └── schema.prisma │ │ └── relation-false │ │ │ ├── expected │ │ │ ├── index.ts │ │ │ ├── post.ts │ │ │ └── user.ts │ │ │ └── prisma │ │ │ ├── .client │ │ │ └── index.ts │ │ │ └── schema.prisma │ ├── regressions.test.ts │ ├── types.test.ts │ └── util.test.ts ├── types.ts └── util.ts ├── tsconfig.json └── yarn.lock /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['14.x', '16.x'] 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | 13 | steps: 14 | - name: Disable Auto CRLF 15 | run: git config --global core.autocrlf false 16 | 17 | - name: Checkout repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Use Node ${{ matrix.node }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node }} 24 | cache: 'yarn' 25 | 26 | - name: Install deps 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Build 30 | run: yarn build 31 | 32 | - name: Lint 33 | run: yarn lint 34 | 35 | - name: Test 36 | run: yarn test --ci --maxWorkers=2 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | examples/prisma/zod 6 | /src/test/functional/*/actual 7 | /src/test/functional/*/prisma/.client/* 8 | !/src/test/functional/*/prisma/.client/index.ts -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | 2 | vscode: 3 | extensions: 4 | - coenraads.bracket-pair-colorizer-2 5 | - mikestead.dotenv 6 | - dbaeumer.vscode-eslint 7 | - github.vscode-pull-request-github 8 | - equinusocio.vsc-material-theme 9 | - equinusocio.vsc-material-theme-icons 10 | - esbenp.prettier-vscode 11 | - prisma.prisma 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint --fix 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn test 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.detectIndentation": false, 4 | "editor.tabSize": 2, 5 | "editor.insertSpaces": false 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Carter Grimmeisen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 21 | 22 | [![NPM][npm-shield]][npm-url] 23 | [![Contributors][contributors-shield]][contributors-url] 24 | [![Forks][forks-shield]][forks-url] 25 | [![Stargazers][stars-shield]][stars-url] 26 | [![Issues][issues-shield]][issues-url] 27 | [![MIT License][license-shield]][license-url] 28 | 29 | 30 |
31 |

32 | 33 | Logo 34 | 35 |

Zod Prisma

36 |

37 | A custom prisma generator that creates Zod schemas from your Prisma model. 38 |
39 | Explore the docs » 40 |
41 |
42 | View Demo 43 | · 44 | Report Bug 45 | · 46 | Request Feature 47 |

48 |

49 | 50 | 51 |
52 |

Table of Contents

53 |
    54 |
  1. 55 | About The Project 56 | 59 |
  2. 60 |
  3. 61 | Getting Started 62 | 66 |
  4. 67 |
  5. Usage 68 | 79 |
  6. 80 |
  7. Examples
  8. 81 |
  9. Roadmap
  10. 82 |
  11. Contributing
  12. 83 |
  13. License
  14. 84 |
  15. Contact
  16. 85 |
86 |
87 | 88 | 89 | 90 | ## About The Project 91 | 92 | I got tired of having to manually create Zod schemas for my Prisma models and of updating them everytime I made schema changes. 93 | This provides a way of automatically generating them with your prisma 94 | 95 | 96 | 97 | ### Built With 98 | 99 | - [dts-cli](https://github.com/weiran-zsd/dts-cli) 100 | - [Zod](https://github.com/colinhacks/zod) 101 | - [Based on this gist](https://gist.github.com/deckchairlabs/8a11c33311c01273deec7e739417dbc9) 102 | 103 | 104 | 105 | ## Getting Started 106 | 107 | To get a local copy up and running follow these simple steps. 108 | 109 | ### Prerequisites 110 | 111 | This project utilizes yarn and if you plan on contributing, you should too. 112 | 113 | ```sh 114 | npm install -g yarn 115 | ``` 116 | 117 | ### Installation 118 | 119 | 0. **Ensure your tsconfig.json enables the compiler's strict mode.** 120 | **Zod requires it and so do we, you will experience TS errors without strict mode enabled** 121 | 122 | 1. Add zod-prisma as a dev dependency 123 | 124 | ```sh 125 | yarn add -D zod-prisma 126 | ``` 127 | 128 | 2. Add the zod-prisma generator to your schema.prisma 129 | 130 | ```prisma 131 | generator zod { 132 | provider = "zod-prisma" 133 | output = "./zod" // (default) the directory where generated zod schemas will be saved 134 | 135 | relationModel = true // (default) Create and export both plain and related models. 136 | // relationModel = "default" // Do not export model without relations. 137 | // relationModel = false // Do not generate related model 138 | 139 | modelCase = "PascalCase" // (default) Output models using pascal case (ex. UserModel, PostModel) 140 | // modelCase = "camelCase" // Output models using camel case (ex. userModel, postModel) 141 | 142 | modelSuffix = "Model" // (default) Suffix to apply to your prisma models when naming Zod schemas 143 | 144 | // useDecimalJs = false // (default) represent the prisma Decimal type using as a JS number 145 | useDecimalJs = true // represent the prisma Decimal type using Decimal.js (as Prisma does) 146 | 147 | imports = null // (default) will import the referenced file in generated schemas to be used via imports.someExportedVariable 148 | 149 | // https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-by-null-values 150 | prismaJsonNullability = true // (default) uses prisma's scheme for JSON field nullability 151 | // prismaJsonNullability = false // allows null assignment to optional JSON fields 152 | } 153 | ``` 154 | 155 | 3. Run `npx prisma generate` or `yarn prisma generate` to generate your zod schemas 156 | 4. Import the generated schemas form your selected output location 157 | 158 | 159 | 160 | ## Usage 161 | 162 | ### JSDoc Generation 163 | 164 | [Rich-comments](https://www.prisma.io/docs/concepts/components/prisma-schema#comments) 165 | in the Prisma schema will be transformed into JSDoc for the associated fields: 166 | 167 | > _Note: make sure to use a triple-slash. Double-slash comments won't be processed._ 168 | 169 | ```prisma 170 | model Post { 171 | /// The unique identifier for the post 172 | /// @default {Generated by database} 173 | id String @id @default(uuid()) 174 | 175 | /// A brief title that describes the contents of the post 176 | title String 177 | 178 | /// The actual contents of the post. 179 | contents String 180 | } 181 | ``` 182 | 183 | Generated code: 184 | 185 | ```ts 186 | export const PostModel = z.object({ 187 | /** 188 | * The unique identifier for the post 189 | * @default {Generated by database} 190 | */ 191 | id: z.string().uuid(), 192 | /** 193 | * A brief title that describes the contents of the post 194 | */ 195 | title: z.string(), 196 | /** 197 | * The actual contents of the post. 198 | */ 199 | contents: z.string(), 200 | }) 201 | ``` 202 | 203 | ### Extending Zod Fields 204 | 205 | You can also use the `@zod` keyword in rich-comments in the Prisma schema 206 | to extend your Zod schema fields: 207 | 208 | ```prisma 209 | model Post { 210 | id String @id @default(uuid()) /// @zod.uuid() 211 | 212 | /// @zod.max(255, { message: "The title must be shorter than 256 characters" }) 213 | title String 214 | 215 | contents String /// @zod.max(10240) 216 | } 217 | ``` 218 | 219 | Generated code: 220 | 221 | ```ts 222 | export const PostModel = z.object({ 223 | id: z.string().uuid(), 224 | title: z.string().max(255, { message: 'The title must be shorter than 256 characters' }), 225 | contents: z.string().max(10240), 226 | }) 227 | ``` 228 | 229 | ### Importing Helpers 230 | 231 | Sometimes its useful to define a custom Zod preprocessor or transformer for your data. 232 | zod-prisma enables you to reuse these by importing them via a config options. For example: 233 | 234 | ```prisma 235 | generator zod { 236 | provider = "zod-prisma" 237 | output = "./zod" 238 | imports = "../src/zod-schemas" 239 | } 240 | 241 | model User { 242 | username String /// @zod.refine(imports.isValidUsername) 243 | } 244 | ``` 245 | 246 | The referenced file can then be used by simply referring to exported members via `imports.whateverExport`. 247 | The generated zod schema files will now include a namespaced import like the following. 248 | 249 | ```typescript 250 | import * as imports from '../../src/zod-schemas' 251 | ``` 252 | 253 | #### Custom Zod Schema 254 | 255 | In conjunction with this import option, you may want to utilize an entirely custom zod schema for a field. 256 | This can be accomplished by using the special comment directive `@zod.custom()`. 257 | By specifying the custom schema within the parentheses you can replace the autogenerated type that would normally be assigned to the field. 258 | 259 | > For instance if you wanted to use `z.preprocess` 260 | 261 | ### JSON Fields 262 | 263 | JSON fields in Prisma disallow null values. This is to disambiguate between setting a field's value to NULL in the database and having 264 | a value of null stored in the JSON. In accordance with this zod-prisma will default to disallowing null values, even if your JSON field is optional. 265 | 266 | If you would like to revert this behavior and allow null assignment to JSON fields, 267 | you can set `prismaJsonNullability` to `false` in the generator options. 268 | 269 | ## Examples 270 | 271 | 272 | 273 | _For examples, please refer to the [Examples Directory](https://github.com/CarterGrimmeisen/zod-prisma/blob/main/examples) or the [Functional Tests](https://github.com/CarterGrimmeisen/zod-prisma/blob/main/src/test/functional)_ 274 | 275 | 276 | 277 | ## Roadmap 278 | 279 | See the [open issues](https://github.com/CarterGrimmeisen/zod-prisma/issues) for a list of proposed features (and known issues). 280 | 281 | 282 | 283 | ## Contributing 284 | 285 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 286 | 287 | 1. Fork the Project 288 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 289 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 290 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 291 | 5. Open a Pull Request 292 | 293 | 294 | 295 | ## License 296 | 297 | Distributed under the MIT License. See `LICENSE` for more information. 298 | 299 | 300 | 301 | ## Contact 302 | 303 | Carter Grimmeisen - Carter.Grimmeisen@uah.edu 304 | 305 | Project Link: [https://github.com/CarterGrimmeisen/zod-prisma](https://github.com/CarterGrimmeisen/zod-prisma) 306 | 307 | 308 | 309 | 310 | [npm-shield]: https://img.shields.io/npm/v/zod-prisma?style=for-the-badge 311 | [npm-url]: https://www.npmjs.com/package/zod-prisma 312 | [contributors-shield]: https://img.shields.io/github/contributors/CarterGrimmeisen/zod-prisma.svg?style=for-the-badge 313 | [contributors-url]: https://github.com/CarterGrimmeisen/zod-prisma/graphs/contributors 314 | [forks-shield]: https://img.shields.io/github/forks/CarterGrimmeisen/zod-prisma.svg?style=for-the-badge 315 | [forks-url]: https://github.com/CarterGrimmeisen/zod-prisma/network/members 316 | [stars-shield]: https://img.shields.io/github/stars/CarterGrimmeisen/zod-prisma.svg?style=for-the-badge 317 | [stars-url]: https://github.com/CarterGrimmeisen/zod-prisma/stargazers 318 | [issues-shield]: https://img.shields.io/github/issues/CarterGrimmeisen/zod-prisma.svg?style=for-the-badge 319 | [issues-url]: https://github.com/CarterGrimmeisen/zod-prisma/issues 320 | [license-shield]: https://img.shields.io/github/license/CarterGrimmeisen/zod-prisma.svg?style=for-the-badge 321 | [license-url]: https://github.com/CarterGrimmeisen/zod-prisma/blob/main/LICENSE 322 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/index') -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | *.env* 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Simple TypeScript Script Example 2 | 3 | This example shows how to use [Prisma Client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client) in a **simple TypeScript script** to read and write data in a SQLite database. You can find the database file with some dummy data at [`./prisma/dev.db`](./prisma/dev.db). 4 | 5 | ## Getting started 6 | 7 | ### 1. Download example and install dependencies 8 | 9 | Download this example: 10 | 11 | ``` 12 | curl https://codeload.github.com/prisma/prisma-examples/tar.gz/latest | tar -xz --strip=2 prisma-examples-latest/typescript/script 13 | ``` 14 | 15 | Install npm dependencies: 16 | 17 | ``` 18 | cd script 19 | npm install 20 | ``` 21 | 22 |
Alternative: Clone the entire repo 23 | 24 | Clone this repository: 25 | 26 | ``` 27 | git clone git@github.com:prisma/prisma-examples.git --depth=1 28 | ``` 29 | 30 | Install npm dependencies: 31 | 32 | ``` 33 | cd prisma-examples/typescript/script 34 | npm install 35 | ``` 36 | 37 |
38 | 39 | ### 2. Create the database 40 | 41 | Run the following command to create your SQLite database file. This also creates the `User` and `Post` tables that are defined in [`prisma/schema.prisma`](./prisma/schema.prisma): 42 | 43 | ``` 44 | npx prisma migrate dev --name init 45 | ``` 46 | 47 | ### 3. Run the script 48 | 49 | Execute the script with this command: 50 | 51 | ``` 52 | npm run dev 53 | ``` 54 | 55 | ## Evolving the app 56 | 57 | Evolving the application typically requires two steps: 58 | 59 | 1. Migrate your database using Prisma Migrate 60 | 1. Update your application code 61 | 62 | For the following example scenario, assume you want to add a "profile" feature to the app where users can create a profile and write a short bio about themselves. 63 | 64 | ### 1. Migrate your database using Prisma Migrate 65 | 66 | The first step is to add a new table, e.g. called `Profile`, to the database. You can do this by adding a new model to your [Prisma schema file](./prisma/schema.prisma) file and then running a migration afterwards: 67 | 68 | ```diff 69 | // schema.prisma 70 | 71 | model Post { 72 | id Int @default(autoincrement()) @id 73 | title String 74 | content String? 75 | published Boolean @default(false) 76 | author User? @relation(fields: [authorId], references: [id]) 77 | authorId Int 78 | } 79 | 80 | model User { 81 | id Int @default(autoincrement()) @id 82 | name String? 83 | email String @unique 84 | posts Post[] 85 | + profile Profile? 86 | } 87 | 88 | +model Profile { 89 | + id Int @default(autoincrement()) @id 90 | + bio String? 91 | + userId Int @unique 92 | + user User @relation(fields: [userId], references: [id]) 93 | +} 94 | ``` 95 | 96 | Once you've updated your data model, you can execute the changes against your database with the following command: 97 | 98 | ``` 99 | npx prisma migrate dev 100 | ``` 101 | 102 | ### 2. Update your application code 103 | 104 | You can now use your `PrismaClient` instance to perform operations against the new `Profile` table. Here are some examples: 105 | 106 | #### Create a new profile for an existing user 107 | 108 | ```ts 109 | const profile = await prisma.profile.create({ 110 | data: { 111 | bio: "Hello World", 112 | user: { 113 | connect: { email: "alice@prisma.io" }, 114 | }, 115 | }, 116 | }); 117 | ``` 118 | 119 | #### Create a new user with a new profile 120 | 121 | ```ts 122 | const user = await prisma.user.create({ 123 | data: { 124 | email: "john@prisma.io", 125 | name: "John", 126 | profile: { 127 | create: { 128 | bio: "Hello World", 129 | }, 130 | }, 131 | }, 132 | }); 133 | ``` 134 | 135 | #### Update the profile of an existing user 136 | 137 | ```ts 138 | const userWithUpdatedProfile = await prisma.user.update({ 139 | where: { email: "alice@prisma.io" }, 140 | data: { 141 | profile: { 142 | update: { 143 | bio: "Hello Friends", 144 | }, 145 | }, 146 | }, 147 | }); 148 | ``` 149 | 150 | 151 | ## Switch to another database (e.g. PostgreSQL, MySQL, SQL Server) 152 | 153 | If you want to try this example with another database than SQLite, you can adjust the the database connection in [`prisma/schema.prisma`](./prisma/schema.prisma) by reconfiguring the `datasource` block. 154 | 155 | Learn more about the different connection configurations in the [docs](https://www.prisma.io/docs/reference/database-reference/connection-urls). 156 | 157 |
Expand for an overview of example configurations with different databases 158 | 159 | ### PostgreSQL 160 | 161 | For PostgreSQL, the connection URL has the following structure: 162 | 163 | ```prisma 164 | datasource db { 165 | provider = "postgresql" 166 | url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA" 167 | } 168 | ``` 169 | 170 | Here is an example connection string with a local PostgreSQL database: 171 | 172 | ```prisma 173 | datasource db { 174 | provider = "postgresql" 175 | url = "postgresql://janedoe:mypassword@localhost:5432/notesapi?schema=public" 176 | } 177 | ``` 178 | 179 | ### MySQL 180 | 181 | For MySQL, the connection URL has the following structure: 182 | 183 | ```prisma 184 | datasource db { 185 | provider = "mysql" 186 | url = "mysql://USER:PASSWORD@HOST:PORT/DATABASE" 187 | } 188 | ``` 189 | 190 | Here is an example connection string with a local MySQL database: 191 | 192 | ```prisma 193 | datasource db { 194 | provider = "mysql" 195 | url = "mysql://janedoe:mypassword@localhost:3306/notesapi" 196 | } 197 | ``` 198 | 199 | ### Microsoft SQL Server (Preview) 200 | 201 | Here is an example connection string with a local Microsoft SQL Server database: 202 | 203 | ```prisma 204 | datasource db { 205 | provider = "sqlserver" 206 | url = "sqlserver://localhost:1433;initial catalog=sample;user=sa;password=mypassword;" 207 | } 208 | ``` 209 | 210 | Because SQL Server is currently in [Preview](https://www.prisma.io/docs/about/releases#preview), you need to specify the `previewFeatures` on your `generator` block: 211 | 212 | ```prisma 213 | generator client { 214 | provider = "prisma-client-js" 215 | previewFeatures = ["microsoftSqlServer"] 216 | } 217 | ``` 218 | 219 |
220 | 221 | ## Next steps 222 | 223 | - Check out the [Prisma docs](https://www.prisma.io/docs) 224 | - Share your feedback in the [`prisma2`](https://prisma.slack.com/messages/CKQTGR6T0/) channel on the [Prisma Slack](https://slack.prisma.io/) 225 | - Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/) 226 | - Watch our biweekly "What's new in Prisma" livestreams on [Youtube](https://www.youtube.com/channel/UCptAHlN1gdwD89tFM3ENb6w) -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script", 3 | "license": "MIT", 4 | "devDependencies": { 5 | "prisma": "2.24.0", 6 | "ts-node": "9.1.1", 7 | "typescript": "4.2.4" 8 | }, 9 | "scripts": { 10 | "dev": "ts-node ./script.ts" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "2.24.0", 14 | "@types/node": "13.13.52", 15 | "zod": "^3.1.0" 16 | }, 17 | "engines": { 18 | "node": ">=10.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = env("PRISMA_DB_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | generator zod { 14 | provider = "../bin/cli.js" 15 | output = "./zod" 16 | relationModel = "default" 17 | } 18 | 19 | model User { 20 | id Int @id @default(autoincrement()) 21 | meta Json 22 | posts Post[] 23 | } 24 | 25 | model Post { 26 | id Int @id @default(autoincrement()) 27 | authorId Int 28 | author User @relation(fields: [authorId], references: [id]) 29 | } 30 | -------------------------------------------------------------------------------- /examples/script.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from '@prisma/client' 2 | import { UserModel } from './prisma/zod' 3 | 4 | const user = UserModel.parse({ 5 | id: 1, 6 | meta: Prisma.JsonNull, 7 | posts: [], 8 | }) 9 | 10 | const prisma = new PrismaClient() 11 | 12 | prisma.user.create({ data: user }).then((created) => console.log(`Successfully created ${created}`)) 13 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "dist", 5 | "strict": true, 6 | "lib": ["esnext"], 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@prisma/client@2.24.0": 6 | version "2.24.0" 7 | resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.24.0.tgz#83404f98905998770625ec7f13ad73532b144eba" 8 | integrity sha512-y3BbJJMB3bhSXWpbqOlnAvnhvK0UYQMZZC5gacS+nR1eZl4MovVgl9syk6En3FXIQaneLukoiWfFaIxfDVZArw== 9 | dependencies: 10 | "@prisma/engines-version" "2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858" 11 | 12 | "@prisma/engines-version@2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858": 13 | version "2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858" 14 | resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858.tgz#609fb53d9feb1efc8689b4ce6098af1b486ad29f" 15 | integrity sha512-KHns2Puc38woxnx3MKoUUW0tfR5yftDCmT/df0rUHe6ZcREt1kwI3dgvsOBz+6n7stuSNeLiU7uEkc7Ga3PgNA== 16 | 17 | "@prisma/engines@2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858": 18 | version "2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858" 19 | resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858.tgz#c4fda8eedb864dd3b7afb8815a6f609d7d1bba2d" 20 | integrity sha512-rcMl4XgkLg1ki94EfRXX6t/Abzw5CMQFkfC6K+dkxuJ9gIo+moGSZHsyYLAD0ccdYhEW0QbP9TNg0VVe8thrNw== 21 | 22 | "@types/node@13.13.52": 23 | version "13.13.52" 24 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" 25 | integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ== 26 | 27 | arg@^4.1.0: 28 | version "4.1.3" 29 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 30 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 31 | 32 | buffer-from@^1.0.0: 33 | version "1.1.1" 34 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 35 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 36 | 37 | create-require@^1.1.0: 38 | version "1.1.1" 39 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 40 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 41 | 42 | diff@^4.0.1: 43 | version "4.0.2" 44 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 45 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 46 | 47 | make-error@^1.1.1: 48 | version "1.3.6" 49 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 50 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 51 | 52 | prisma@2.24.0: 53 | version "2.24.0" 54 | resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.24.0.tgz#7f2b25a11bd73dc1f9ab88e2fd92ff5fdddee85b" 55 | integrity sha512-kZBYxAkThSFfAZzTGpsihaNqNbGQVzkPhQ0XklLDetN36EpQXeeOQhS05DrzlGSQ7rG7w8mt3m3iDF0S/RD/qw== 56 | dependencies: 57 | "@prisma/engines" "2.24.0-30.f3e341280d96d0abc068f97e959ddf01f321a858" 58 | 59 | source-map-support@^0.5.17: 60 | version "0.5.19" 61 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" 62 | integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== 63 | dependencies: 64 | buffer-from "^1.0.0" 65 | source-map "^0.6.0" 66 | 67 | source-map@^0.6.0: 68 | version "0.6.1" 69 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 70 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 71 | 72 | ts-node@9.1.1: 73 | version "9.1.1" 74 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" 75 | integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== 76 | dependencies: 77 | arg "^4.1.0" 78 | create-require "^1.1.0" 79 | diff "^4.0.1" 80 | make-error "^1.1.1" 81 | source-map-support "^0.5.17" 82 | yn "3.1.1" 83 | 84 | typescript@4.2.4: 85 | version "4.2.4" 86 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" 87 | integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== 88 | 89 | yn@3.1.1: 90 | version "3.1.1" 91 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 92 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 93 | 94 | zod@^3.1.0: 95 | version "3.1.0" 96 | resolved "https://registry.yarnpkg.com/zod/-/zod-3.1.0.tgz#b9b6c0f949f9b54eb2c32cbbe81e9d0f24a143d8" 97 | integrity sha512-qS0an8oo9EvVLVqIVxMZrQrfR2pVwBtlPp+BzTB/F19IyPTRaLLoFfdXRzgh626pxFR1efuTWV8bPoEE58KwqA== 98 | -------------------------------------------------------------------------------- /images/zod-prisma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Drop Shadow 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zod-prisma", 3 | "version": "0.5.4", 4 | "description": "A Prisma generator that creates Zod schemas for all of your models", 5 | "license": "MIT", 6 | "author": "Carter Grimmeisen", 7 | "homepage": "https://github.com/CarterGrimmeisen/zod-prisma#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/CarterGrimmeisen/zod-prisma.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/CarterGrimmeisen/zod-prisma/issues" 14 | }, 15 | "main": "dist/index.js", 16 | "module": "dist/zod-prisma.esm.js", 17 | "typings": "dist/index.d.ts", 18 | "bin": { 19 | "zod-prisma": "bin/cli.js" 20 | }, 21 | "keywords": [ 22 | "zod", 23 | "prisma", 24 | "generator" 25 | ], 26 | "files": [ 27 | "bin", 28 | "dist" 29 | ], 30 | "scripts": { 31 | "build": "dts build --target node --format cjs --rollupTypes", 32 | "lint": "tsc --noEmit && dts lint src --ignore-pattern src/test/functional", 33 | "prepare": "husky install", 34 | "prepublish": "dts build --target node --format cjs --rollupTypes", 35 | "start": "dts watch", 36 | "test": "dts test --maxWorkers=4 --verbose" 37 | }, 38 | "prettier": { 39 | "printWidth": 100, 40 | "semi": false, 41 | "singleQuote": true, 42 | "tabWidth": 4, 43 | "trailingComma": "es5", 44 | "useTabs": true 45 | }, 46 | "eslintConfig": { 47 | "rules": { 48 | "react-hooks/rules-of-hooks": "off" 49 | } 50 | }, 51 | "jest": { 52 | "testEnvironment": "node" 53 | }, 54 | "dependencies": { 55 | "@prisma/generator-helper": "~3.8.1", 56 | "parenthesis": "^3.1.8", 57 | "ts-morph": "^13.0.2" 58 | }, 59 | "devDependencies": { 60 | "@prisma/client": "~3.8.1", 61 | "@prisma/sdk": "~3.7.0", 62 | "@tsconfig/recommended": "^1.0.1", 63 | "@types/fs-extra": "^9.0.13", 64 | "dts-cli": "^1.1.5", 65 | "execa": "^5.1.0", 66 | "fast-glob": "^3.2.5", 67 | "fs-extra": "^10.0.0", 68 | "husky": "^7.0.4", 69 | "jest-mock-extended": "^2.0.4", 70 | "prisma": "^3.4.2", 71 | "tslib": "^2.3.1", 72 | "typescript": "^4.5.4", 73 | "zod": "^3.11.6" 74 | }, 75 | "peerDependencies": { 76 | "decimal.js": "^10.0.0", 77 | "prisma": "^3.0.0", 78 | "zod": "^3.0.0" 79 | }, 80 | "peerDependenciesMeta": { 81 | "decimal.js": { 82 | "optional": true 83 | } 84 | }, 85 | "engines": { 86 | "node": ">=14" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | const configBoolean = z.enum(['true', 'false']).transform((arg) => JSON.parse(arg)) 4 | 5 | export const configSchema = z.object({ 6 | relationModel: configBoolean.default('true').or(z.literal('default')), 7 | modelSuffix: z.string().default('Model'), 8 | modelCase: z.enum(['PascalCase', 'camelCase']).default('PascalCase'), 9 | useDecimalJs: configBoolean.default('false'), 10 | imports: z.string().optional(), 11 | prismaJsonNullability: configBoolean.default('true'), 12 | }) 13 | 14 | export type Config = z.infer 15 | 16 | export type PrismaOptions = { 17 | schemaPath: string 18 | outputPath: string 19 | clientPath: string 20 | } 21 | 22 | export type Names = { 23 | model: string 24 | related: string 25 | } 26 | -------------------------------------------------------------------------------- /src/docs.ts: -------------------------------------------------------------------------------- 1 | import { ArrayTree, parse, stringify } from 'parenthesis' 2 | import { chunk } from './util' 3 | 4 | export const getJSDocs = (docString?: string) => { 5 | const lines: string[] = [] 6 | 7 | if (docString) { 8 | const docLines = docString.split('\n').filter((dL) => !dL.trimStart().startsWith('@zod')) 9 | 10 | if (docLines.length) { 11 | lines.push('/**') 12 | docLines.forEach((dL) => lines.push(` * ${dL}`)) 13 | lines.push(' */') 14 | } 15 | } 16 | 17 | return lines 18 | } 19 | 20 | export const getZodDocElements = (docString: string) => 21 | docString 22 | .split('\n') 23 | .filter((line) => line.trimStart().startsWith('@zod')) 24 | .map((line) => line.trimStart().slice(4)) 25 | .flatMap((line) => 26 | // Array.from(line.matchAll(/\.([^().]+\(.*?\))/g), (m) => m.slice(1)).flat() 27 | chunk(parse(line), 2) 28 | .slice(0, -1) 29 | .map( 30 | ([each, contents]) => 31 | (each as string).replace(/\)?\./, '') + 32 | `${stringify(contents as ArrayTree)})` 33 | ) 34 | ) 35 | 36 | export const computeCustomSchema = (docString: string) => { 37 | return getZodDocElements(docString) 38 | .find((modifier) => modifier.startsWith('custom(')) 39 | ?.slice(7) 40 | .slice(0, -1) 41 | } 42 | 43 | export const computeModifiers = (docString: string) => { 44 | return getZodDocElements(docString).filter((each) => !each.startsWith('custom(')) 45 | } 46 | -------------------------------------------------------------------------------- /src/generator.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { DMMF } from '@prisma/generator-helper' 3 | import { 4 | ImportDeclarationStructure, 5 | SourceFile, 6 | StructureKind, 7 | VariableDeclarationKind, 8 | } from 'ts-morph' 9 | import { Config, PrismaOptions } from './config' 10 | import { dotSlash, needsRelatedModel, useModelNames, writeArray } from './util' 11 | import { getJSDocs } from './docs' 12 | import { getZodConstructor } from './types' 13 | 14 | export const writeImportsForModel = ( 15 | model: DMMF.Model, 16 | sourceFile: SourceFile, 17 | config: Config, 18 | { schemaPath, outputPath, clientPath }: PrismaOptions 19 | ) => { 20 | const { relatedModelName } = useModelNames(config) 21 | const importList: ImportDeclarationStructure[] = [ 22 | { 23 | kind: StructureKind.ImportDeclaration, 24 | namespaceImport: 'z', 25 | moduleSpecifier: 'zod', 26 | }, 27 | ] 28 | 29 | if (config.imports) { 30 | importList.push({ 31 | kind: StructureKind.ImportDeclaration, 32 | namespaceImport: 'imports', 33 | moduleSpecifier: dotSlash( 34 | path.relative(outputPath, path.resolve(path.dirname(schemaPath), config.imports)) 35 | ), 36 | }) 37 | } 38 | 39 | if (config.useDecimalJs && model.fields.some((f) => f.type === 'Decimal')) { 40 | importList.push({ 41 | kind: StructureKind.ImportDeclaration, 42 | namedImports: ['Decimal'], 43 | moduleSpecifier: 'decimal.js', 44 | }) 45 | } 46 | 47 | const enumFields = model.fields.filter((f) => f.kind === 'enum') 48 | const relationFields = model.fields.filter((f) => f.kind === 'object') 49 | const relativePath = path.relative(outputPath, clientPath) 50 | 51 | if (enumFields.length > 0) { 52 | importList.push({ 53 | kind: StructureKind.ImportDeclaration, 54 | isTypeOnly: enumFields.length === 0, 55 | moduleSpecifier: dotSlash(relativePath), 56 | namedImports: enumFields.map((f) => f.type), 57 | }) 58 | } 59 | 60 | if (config.relationModel !== false && relationFields.length > 0) { 61 | const filteredFields = relationFields.filter((f) => f.type !== model.name) 62 | 63 | if (filteredFields.length > 0) { 64 | importList.push({ 65 | kind: StructureKind.ImportDeclaration, 66 | moduleSpecifier: './index', 67 | namedImports: Array.from( 68 | new Set( 69 | filteredFields.flatMap((f) => [ 70 | `Complete${f.type}`, 71 | relatedModelName(f.type), 72 | ]) 73 | ) 74 | ), 75 | }) 76 | } 77 | } 78 | 79 | sourceFile.addImportDeclarations(importList) 80 | } 81 | 82 | export const writeTypeSpecificSchemas = ( 83 | model: DMMF.Model, 84 | sourceFile: SourceFile, 85 | config: Config, 86 | _prismaOptions: PrismaOptions 87 | ) => { 88 | if (model.fields.some((f) => f.type === 'Json')) { 89 | sourceFile.addStatements((writer) => { 90 | writer.newLine() 91 | writeArray(writer, [ 92 | '// Helper schema for JSON fields', 93 | `type Literal = boolean | number | string${ 94 | config.prismaJsonNullability ? '' : '| null' 95 | }`, 96 | 'type Json = Literal | { [key: string]: Json } | Json[]', 97 | `const literalSchema = z.union([z.string(), z.number(), z.boolean()${ 98 | config.prismaJsonNullability ? '' : ', z.null()' 99 | }])`, 100 | 'const jsonSchema: z.ZodSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))', 101 | ]) 102 | }) 103 | } 104 | 105 | if (config.useDecimalJs && model.fields.some((f) => f.type === 'Decimal')) { 106 | sourceFile.addStatements((writer) => { 107 | writer.newLine() 108 | writeArray(writer, [ 109 | '// Helper schema for Decimal fields', 110 | 'z', 111 | '.instanceof(Decimal)', 112 | '.or(z.string())', 113 | '.or(z.number())', 114 | '.refine((value) => {', 115 | ' try {', 116 | ' return new Decimal(value);', 117 | ' } catch (error) {', 118 | ' return false;', 119 | ' }', 120 | '})', 121 | '.transform((value) => new Decimal(value));', 122 | ]) 123 | }) 124 | } 125 | } 126 | 127 | export const generateSchemaForModel = ( 128 | model: DMMF.Model, 129 | sourceFile: SourceFile, 130 | config: Config, 131 | _prismaOptions: PrismaOptions 132 | ) => { 133 | const { modelName } = useModelNames(config) 134 | 135 | sourceFile.addVariableStatement({ 136 | declarationKind: VariableDeclarationKind.Const, 137 | isExported: true, 138 | leadingTrivia: (writer) => writer.blankLineIfLastNot(), 139 | declarations: [ 140 | { 141 | name: modelName(model.name), 142 | initializer(writer) { 143 | writer 144 | .write('z.object(') 145 | .inlineBlock(() => { 146 | model.fields 147 | .filter((f) => f.kind !== 'object') 148 | .forEach((field) => { 149 | writeArray(writer, getJSDocs(field.documentation)) 150 | writer 151 | .write(`${field.name}: ${getZodConstructor(field)}`) 152 | .write(',') 153 | .newLine() 154 | }) 155 | }) 156 | .write(')') 157 | }, 158 | }, 159 | ], 160 | }) 161 | } 162 | 163 | export const generateRelatedSchemaForModel = ( 164 | model: DMMF.Model, 165 | sourceFile: SourceFile, 166 | config: Config, 167 | _prismaOptions: PrismaOptions 168 | ) => { 169 | const { modelName, relatedModelName } = useModelNames(config) 170 | 171 | const relationFields = model.fields.filter((f) => f.kind === 'object') 172 | 173 | sourceFile.addInterface({ 174 | name: `Complete${model.name}`, 175 | isExported: true, 176 | extends: [`z.infer`], 177 | properties: relationFields.map((f) => ({ 178 | hasQuestionToken: !f.isRequired, 179 | name: f.name, 180 | type: `Complete${f.type}${f.isList ? '[]' : ''}${!f.isRequired ? ' | null' : ''}`, 181 | })), 182 | }) 183 | 184 | sourceFile.addStatements((writer) => 185 | writeArray(writer, [ 186 | '', 187 | '/**', 188 | ` * ${relatedModelName( 189 | model.name 190 | )} contains all relations on your model in addition to the scalars`, 191 | ' *', 192 | ' * NOTE: Lazy required in case of potential circular dependencies within schema', 193 | ' */', 194 | ]) 195 | ) 196 | 197 | sourceFile.addVariableStatement({ 198 | declarationKind: VariableDeclarationKind.Const, 199 | isExported: true, 200 | declarations: [ 201 | { 202 | name: relatedModelName(model.name), 203 | type: `z.ZodSchema`, 204 | initializer(writer) { 205 | writer 206 | .write(`z.lazy(() => ${modelName(model.name)}.extend(`) 207 | .inlineBlock(() => { 208 | relationFields.forEach((field) => { 209 | writeArray(writer, getJSDocs(field.documentation)) 210 | 211 | writer 212 | .write( 213 | `${field.name}: ${getZodConstructor( 214 | field, 215 | relatedModelName 216 | )}` 217 | ) 218 | .write(',') 219 | .newLine() 220 | }) 221 | }) 222 | .write('))') 223 | }, 224 | }, 225 | ], 226 | }) 227 | } 228 | 229 | export const populateModelFile = ( 230 | model: DMMF.Model, 231 | sourceFile: SourceFile, 232 | config: Config, 233 | prismaOptions: PrismaOptions 234 | ) => { 235 | writeImportsForModel(model, sourceFile, config, prismaOptions) 236 | writeTypeSpecificSchemas(model, sourceFile, config, prismaOptions) 237 | generateSchemaForModel(model, sourceFile, config, prismaOptions) 238 | if (needsRelatedModel(model, config)) 239 | generateRelatedSchemaForModel(model, sourceFile, config, prismaOptions) 240 | } 241 | 242 | export const generateBarrelFile = (models: DMMF.Model[], indexFile: SourceFile) => { 243 | models.forEach((model) => 244 | indexFile.addExportDeclaration({ 245 | moduleSpecifier: `./${model.name.toLowerCase()}`, 246 | }) 247 | ) 248 | } 249 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore Importing package.json for automated synchronization of version numbers 2 | import { version } from '../package.json' 3 | 4 | import { generatorHandler } from '@prisma/generator-helper' 5 | import { SemicolonPreference } from 'typescript' 6 | import { configSchema, PrismaOptions } from './config' 7 | import { populateModelFile, generateBarrelFile } from './generator' 8 | import { Project } from 'ts-morph' 9 | 10 | generatorHandler({ 11 | onManifest() { 12 | return { 13 | version, 14 | prettyName: 'Zod Schemas', 15 | defaultOutput: 'zod', 16 | } 17 | }, 18 | onGenerate(options) { 19 | const project = new Project() 20 | 21 | const models = options.dmmf.datamodel.models 22 | 23 | const { schemaPath } = options 24 | const outputPath = options.generator.output!.value 25 | const clientPath = options.otherGenerators.find( 26 | (each) => each.provider.value === 'prisma-client-js' 27 | )!.output!.value! 28 | 29 | const results = configSchema.safeParse(options.generator.config) 30 | if (!results.success) 31 | throw new Error( 32 | 'Incorrect config provided. Please check the values you provided and try again.' 33 | ) 34 | 35 | const config = results.data 36 | const prismaOptions: PrismaOptions = { 37 | clientPath, 38 | outputPath, 39 | schemaPath, 40 | } 41 | 42 | const indexFile = project.createSourceFile( 43 | `${outputPath}/index.ts`, 44 | {}, 45 | { overwrite: true } 46 | ) 47 | 48 | generateBarrelFile(models, indexFile) 49 | 50 | indexFile.formatText({ 51 | indentSize: 2, 52 | convertTabsToSpaces: true, 53 | semicolons: SemicolonPreference.Remove, 54 | }) 55 | 56 | models.forEach((model) => { 57 | const sourceFile = project.createSourceFile( 58 | `${outputPath}/${model.name.toLowerCase()}.ts`, 59 | {}, 60 | { overwrite: true } 61 | ) 62 | 63 | populateModelFile(model, sourceFile, config, prismaOptions) 64 | 65 | sourceFile.formatText({ 66 | indentSize: 2, 67 | convertTabsToSpaces: true, 68 | semicolons: SemicolonPreference.Remove, 69 | }) 70 | }) 71 | 72 | return project.save() 73 | }, 74 | }) 75 | -------------------------------------------------------------------------------- /src/test/docs.test.ts: -------------------------------------------------------------------------------- 1 | import { computeCustomSchema, computeModifiers, getJSDocs } from '../docs' 2 | 3 | describe('docs Package', () => { 4 | test('computeModifiers', () => { 5 | const modifiers = computeModifiers(` 6 | @zod.email().optional() 7 | @zod.url() 8 | @zod.uuid() 9 | @zod.min(12) 10 | @zod.refine((val) => val !== 14) 11 | Banana 12 | @example something something 13 | `) 14 | 15 | expect(modifiers).toStrictEqual([ 16 | 'email()', 17 | 'optional()', 18 | 'url()', 19 | 'uuid()', 20 | 'min(12)', 21 | 'refine((val) => val !== 14)', 22 | ]) 23 | }) 24 | 25 | test('Regression #86', () => { 26 | const customSchema = computeCustomSchema(` 27 | @zod.custom(z.string().min(1).refine((val) => isURL(val))) 28 | `) 29 | 30 | expect(customSchema).toBe('z.string().min(1).refine((val) => isURL(val))') 31 | }) 32 | 33 | test('getJSDocs', () => { 34 | const docLines = getJSDocs( 35 | ['This is something', 'How about something else', '@something', '@example ur mom'].join( 36 | '\n' 37 | ) 38 | ) 39 | 40 | expect(docLines.length).toBe(6) 41 | expect(docLines).toStrictEqual([ 42 | '/**', 43 | ' * This is something', 44 | ' * How about something else', 45 | ' * @something', 46 | ' * @example ur mom', 47 | ' */', 48 | ]) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/test/functional/basic/expected/document.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DocumentModel = z.object({ 4 | id: z.string(), 5 | filename: z.string(), 6 | author: z.string(), 7 | contents: z.string(), 8 | created: z.date(), 9 | updated: z.date(), 10 | }) 11 | -------------------------------------------------------------------------------- /src/test/functional/basic/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./document" 2 | export * from "./presentation" 3 | export * from "./spreadsheet" 4 | -------------------------------------------------------------------------------- /src/test/functional/basic/expected/presentation.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const PresentationModel = z.object({ 4 | id: z.string(), 5 | filename: z.string(), 6 | author: z.string(), 7 | contents: z.string().array(), 8 | created: z.date(), 9 | updated: z.date(), 10 | }) 11 | -------------------------------------------------------------------------------- /src/test/functional/basic/expected/spreadsheet.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | // Helper schema for JSON fields 4 | type Literal = boolean | number | string 5 | type Json = Literal | { [key: string]: Json } | Json[] 6 | const literalSchema = z.union([z.string(), z.number(), z.boolean()]) 7 | const jsonSchema: z.ZodSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) 8 | 9 | export const SpreadsheetModel = z.object({ 10 | id: z.string(), 11 | filename: z.string(), 12 | author: z.string(), 13 | contents: jsonSchema, 14 | created: z.date(), 15 | updated: z.date(), 16 | }) 17 | -------------------------------------------------------------------------------- /src/test/functional/basic/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility Types 3 | */ 4 | 5 | /** 6 | * From https://github.com/sindresorhus/type-fest/ 7 | * Matches a JSON object. 8 | * This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. 9 | */ 10 | export type JsonObject = { [Key in string]?: JsonValue } 11 | 12 | /** 13 | * From https://github.com/sindresorhus/type-fest/ 14 | * Matches a JSON array. 15 | */ 16 | export interface JsonArray extends Array {} 17 | 18 | /** 19 | * From https://github.com/sindresorhus/type-fest/ 20 | * Matches any valid JSON value. 21 | */ 22 | export type JsonValue = string | number | boolean | JsonObject | JsonArray | null 23 | 24 | /** 25 | * Model Document 26 | * 27 | */ 28 | export type Document = { 29 | id: string 30 | filename: string 31 | author: string 32 | contents: string 33 | created: Date 34 | updated: Date 35 | } 36 | 37 | /** 38 | * Model Presentation 39 | * 40 | */ 41 | export type Presentation = { 42 | id: string 43 | filename: string 44 | author: string 45 | contents: string[] 46 | created: Date 47 | updated: Date 48 | } 49 | 50 | /** 51 | * Model Spreadsheet 52 | * 53 | */ 54 | export type Spreadsheet = { 55 | id: string 56 | filename: string 57 | author: string 58 | contents: JsonValue 59 | created: Date 60 | updated: Date 61 | } 62 | -------------------------------------------------------------------------------- /src/test/functional/basic/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | } 18 | 19 | model Document { 20 | id String @id @default(cuid()) 21 | filename String @unique 22 | author String 23 | contents String 24 | 25 | created DateTime @default(now()) 26 | updated DateTime @default(now()) 27 | } 28 | 29 | model Presentation { 30 | id String @id @default(cuid()) 31 | filename String @unique 32 | author String 33 | contents String[] 34 | 35 | created DateTime @default(now()) 36 | updated DateTime @default(now()) 37 | } 38 | 39 | model Spreadsheet { 40 | id String @id @default(cuid()) 41 | filename String @unique 42 | author String 43 | contents Json 44 | 45 | created DateTime @default(now()) 46 | updated DateTime @default(now()) 47 | } 48 | -------------------------------------------------------------------------------- /src/test/functional/config-import/expected/document.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import * as imports from "../prisma/zod-utils" 3 | 4 | export const DocumentModel = z.object({ 5 | id: z.string(), 6 | filename: z.string(), 7 | author: z.string(), 8 | contents: z.string(), 9 | size: imports.decimalSchema, 10 | created: z.date(), 11 | updated: z.date(), 12 | }) 13 | -------------------------------------------------------------------------------- /src/test/functional/config-import/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./document" 2 | -------------------------------------------------------------------------------- /src/test/functional/config-import/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | import { Decimal } from 'decimal.js' 2 | 3 | /** 4 | * Model Document 5 | * 6 | */ 7 | export type Document = { 8 | id: string 9 | filename: string 10 | author: string 11 | contents: string 12 | /** 13 | * @zod.custom(imports.decimalSchema) 14 | */ 15 | size: Decimal 16 | created: Date 17 | updated: Date 18 | } 19 | -------------------------------------------------------------------------------- /src/test/functional/config-import/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = "" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | output = ".client" 9 | } 10 | 11 | generator zod { 12 | provider = "zod-prisma" 13 | output = "../actual/" 14 | imports = "./zod-utils" 15 | } 16 | 17 | model Document { 18 | id String @id @default(cuid()) 19 | filename String @unique 20 | author String 21 | contents String 22 | size Decimal /// @zod.custom(imports.decimalSchema) 23 | 24 | created DateTime @default(now()) 25 | updated DateTime @default(now()) 26 | } 27 | -------------------------------------------------------------------------------- /src/test/functional/config-import/prisma/zod-utils.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { Decimal } from 'decimal.js' 3 | 4 | export const decimalSchema = z 5 | .union([z.string(), z.number()]) 6 | .transform((value) => new Decimal(value)) 7 | -------------------------------------------------------------------------------- /src/test/functional/config/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user" 2 | export * from "./post" 3 | -------------------------------------------------------------------------------- /src/test/functional/config/expected/post.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompleteUser, userSchema } from "./index" 3 | 4 | export const _postSchema = z.object({ 5 | id: z.string(), 6 | title: z.string(), 7 | contents: z.string(), 8 | userId: z.string(), 9 | }) 10 | 11 | export interface CompletePost extends z.infer { 12 | author: CompleteUser 13 | } 14 | 15 | /** 16 | * postSchema contains all relations on your model in addition to the scalars 17 | * 18 | * NOTE: Lazy required in case of potential circular dependencies within schema 19 | */ 20 | export const postSchema: z.ZodSchema = z.lazy(() => _postSchema.extend({ 21 | author: userSchema, 22 | })) 23 | -------------------------------------------------------------------------------- /src/test/functional/config/expected/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompletePost, postSchema } from "./index" 3 | 4 | export const _userSchema = z.object({ 5 | id: z.string(), 6 | name: z.string(), 7 | email: z.string(), 8 | }) 9 | 10 | export interface CompleteUser extends z.infer { 11 | posts: CompletePost[] 12 | } 13 | 14 | /** 15 | * userSchema contains all relations on your model in addition to the scalars 16 | * 17 | * NOTE: Lazy required in case of potential circular dependencies within schema 18 | */ 19 | export const userSchema: z.ZodSchema = z.lazy(() => _userSchema.extend({ 20 | posts: postSchema.array(), 21 | })) 22 | -------------------------------------------------------------------------------- /src/test/functional/config/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Model User 3 | * 4 | */ 5 | export type User = { 6 | id: string 7 | name: string 8 | email: string 9 | } 10 | 11 | /** 12 | * Model Post 13 | * 14 | */ 15 | export type Post = { 16 | id: string 17 | title: string 18 | contents: string 19 | userId: string 20 | } 21 | -------------------------------------------------------------------------------- /src/test/functional/config/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | relationModel = "default" 18 | modelCase = "camelCase" 19 | modelSuffix = "Schema" 20 | } 21 | 22 | model User { 23 | id String @id @default(cuid()) 24 | name String 25 | email String 26 | posts Post[] 27 | } 28 | 29 | model Post { 30 | id String @id @default(cuid()) 31 | title String 32 | contents String 33 | author User @relation(fields: [userId], references: [id]) 34 | userId String 35 | } 36 | -------------------------------------------------------------------------------- /src/test/functional/different-client-path/expected/document.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DocumentModel = z.object({ 4 | id: z.string(), 5 | filename: z.string(), 6 | author: z.string(), 7 | contents: z.string(), 8 | created: z.date(), 9 | updated: z.date(), 10 | }) 11 | -------------------------------------------------------------------------------- /src/test/functional/different-client-path/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./document" 2 | -------------------------------------------------------------------------------- /src/test/functional/different-client-path/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Model Document 3 | * 4 | */ 5 | export type Document = { 6 | id: string 7 | filename: string 8 | author: string 9 | contents: string 10 | created: Date 11 | updated: Date 12 | } 13 | -------------------------------------------------------------------------------- /src/test/functional/different-client-path/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | } 18 | 19 | model Document { 20 | id String @id @default(cuid()) 21 | filename String @unique 22 | author String 23 | contents String 24 | 25 | created DateTime @default(now()) 26 | updated DateTime @default(now()) 27 | } 28 | -------------------------------------------------------------------------------- /src/test/functional/docs/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./post" 2 | -------------------------------------------------------------------------------- /src/test/functional/docs/expected/post.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const PostModel = z.object({ 4 | /** 5 | * The unique identifier for the post 6 | * @default {Generated by database} 7 | */ 8 | id: z.string().uuid(), 9 | /** 10 | * A brief title that describes the contents of the post 11 | */ 12 | title: z.string().max(255, { message: "The title must be shorter than 256 characters" }), 13 | /** 14 | * The actual contents of the post. 15 | */ 16 | contents: z.string().max(10240), 17 | }) 18 | -------------------------------------------------------------------------------- /src/test/functional/docs/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Model Post 3 | * 4 | */ 5 | export type Post = { 6 | /** 7 | * The unique identifier for the post 8 | * @zod.uuid() 9 | * @default {Generated by database} 10 | */ 11 | id: string 12 | /** 13 | * A brief title that describes the contents of the post 14 | * @zod.max(255, { message: "The title must be shorter than 256 characters" }) 15 | */ 16 | title: string 17 | /** 18 | * The actual contents of the post. 19 | * @zod.max(10240) 20 | */ 21 | contents: string 22 | } 23 | -------------------------------------------------------------------------------- /src/test/functional/docs/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | } 18 | 19 | model Post { 20 | /// The unique identifier for the post 21 | /// @zod.uuid() 22 | /// @default {Generated by database} 23 | id String @id @default(uuid()) 24 | 25 | /// A brief title that describes the contents of the post 26 | /// @zod.max(255, { message: "The title must be shorter than 256 characters" }) 27 | title String 28 | 29 | /// The actual contents of the post. 30 | /// @zod.max(10240) 31 | contents String 32 | } 33 | -------------------------------------------------------------------------------- /src/test/functional/driver.test.ts: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob' 2 | import execa from 'execa' 3 | import { getDMMF, getConfig } from '@prisma/sdk' 4 | import { readFile } from 'fs-extra' 5 | import path from 'path' 6 | import { Project } from 'ts-morph' 7 | import { SemicolonPreference } from 'typescript' 8 | import { configSchema, PrismaOptions } from '../../config' 9 | import { populateModelFile, generateBarrelFile } from '../../generator' 10 | 11 | jest.setTimeout(10000) 12 | 13 | const ftForDir = (dir: string) => async () => { 14 | const schemaFile = path.resolve(__dirname, dir, 'prisma/schema.prisma') 15 | const expectedDir = path.resolve(__dirname, dir, 'expected') 16 | const actualDir = path.resolve(__dirname, dir, 'actual') 17 | 18 | const project = new Project() 19 | 20 | const datamodel = await readFile(schemaFile, 'utf-8') 21 | 22 | const dmmf = await getDMMF({ 23 | datamodel, 24 | }) 25 | 26 | const { generators } = await getConfig({ 27 | datamodel, 28 | }) 29 | 30 | const generator = generators.find((generator) => generator.provider.value === 'zod-prisma')! 31 | const config = configSchema.parse(generator.config) 32 | 33 | const prismaClient = generators.find( 34 | (generator) => generator.provider.value === 'prisma-client-js' 35 | )! 36 | 37 | const outputPath = path.resolve(path.dirname(schemaFile), generator.output!.value) 38 | const clientPath = path.resolve(path.dirname(schemaFile), prismaClient.output!.value) 39 | 40 | const prismaOptions: PrismaOptions = { 41 | clientPath, 42 | outputPath, 43 | schemaPath: schemaFile, 44 | } 45 | 46 | const indexFile = project.createSourceFile(`${outputPath}/index.ts`, {}, { overwrite: true }) 47 | 48 | generateBarrelFile(dmmf.datamodel.models, indexFile) 49 | 50 | indexFile.formatText({ 51 | indentSize: 2, 52 | convertTabsToSpaces: true, 53 | semicolons: SemicolonPreference.Remove, 54 | }) 55 | 56 | await indexFile.save() 57 | 58 | const actualIndexContents = await readFile(`${actualDir}/index.ts`, 'utf-8') 59 | 60 | const expectedIndexFile = path.resolve(expectedDir, `index.ts`) 61 | const expectedIndexContents = await readFile( 62 | path.resolve(expectedDir, expectedIndexFile), 63 | 'utf-8' 64 | ) 65 | 66 | expect(actualIndexContents).toStrictEqual(expectedIndexContents) 67 | 68 | await Promise.all( 69 | dmmf.datamodel.models.map(async (model) => { 70 | const sourceFile = project.createSourceFile( 71 | `${actualDir}/${model.name.toLowerCase()}.ts`, 72 | {}, 73 | { overwrite: true } 74 | ) 75 | 76 | populateModelFile(model, sourceFile, config, prismaOptions) 77 | 78 | sourceFile.formatText({ 79 | indentSize: 2, 80 | convertTabsToSpaces: true, 81 | semicolons: SemicolonPreference.Remove, 82 | }) 83 | 84 | await sourceFile.save() 85 | const actualContents = await readFile( 86 | `${actualDir}/${model.name.toLowerCase()}.ts`, 87 | 'utf-8' 88 | ) 89 | 90 | const expectedFile = path.resolve(expectedDir, `${model.name.toLowerCase()}.ts`) 91 | const expectedContents = await readFile( 92 | path.resolve(expectedDir, expectedFile), 93 | 'utf-8' 94 | ) 95 | 96 | expect(actualContents).toStrictEqual(expectedContents) 97 | }) 98 | ) 99 | 100 | await project.save() 101 | } 102 | 103 | describe('Functional Tests', () => { 104 | test.concurrent('Basic', ftForDir('basic')) 105 | test.concurrent('Config', ftForDir('config')) 106 | test.concurrent('Docs', ftForDir('docs')) 107 | test.concurrent('Different Client Path', ftForDir('different-client-path')) 108 | test.concurrent('Recursive Schema', ftForDir('recursive')) 109 | test.concurrent('relationModel = false', ftForDir('relation-false')) 110 | test.concurrent('Relation - 1 to 1', ftForDir('relation-1to1')) 111 | test.concurrent('Imports', ftForDir('imports')) 112 | test.concurrent('JSON', ftForDir('json')) 113 | test.concurrent('Optional fields', ftForDir('optional')) 114 | test.concurrent('Config Import', ftForDir('config-import')) 115 | 116 | test.concurrent('Type Check Everything', async () => { 117 | const typeCheckResults = await execa( 118 | path.resolve(__dirname, '../../../node_modules/.bin/tsc'), 119 | ['--strict', '--noEmit', ...(await glob(`${__dirname}/*/expected/*.ts`))] 120 | ) 121 | 122 | expect(typeCheckResults.exitCode).toBe(0) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /src/test/functional/imports/expected/document.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { Status } from "../prisma/.client" 3 | 4 | export const DocumentModel = z.object({ 5 | id: z.string(), 6 | filename: z.string(), 7 | author: z.string(), 8 | contents: z.string(), 9 | status: z.nativeEnum(Status), 10 | created: z.date(), 11 | updated: z.date(), 12 | }) 13 | -------------------------------------------------------------------------------- /src/test/functional/imports/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./document" 2 | export * from "./presentation" 3 | export * from "./spreadsheet" 4 | -------------------------------------------------------------------------------- /src/test/functional/imports/expected/presentation.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompleteSpreadsheet, RelatedSpreadsheetModel } from "./index" 3 | 4 | export const PresentationModel = z.object({ 5 | id: z.string(), 6 | filename: z.string(), 7 | author: z.string(), 8 | contents: z.string().array(), 9 | created: z.date(), 10 | updated: z.date(), 11 | }) 12 | 13 | export interface CompletePresentation extends z.infer { 14 | spreadsheets: CompleteSpreadsheet[] 15 | } 16 | 17 | /** 18 | * RelatedPresentationModel contains all relations on your model in addition to the scalars 19 | * 20 | * NOTE: Lazy required in case of potential circular dependencies within schema 21 | */ 22 | export const RelatedPresentationModel: z.ZodSchema = z.lazy(() => PresentationModel.extend({ 23 | spreadsheets: RelatedSpreadsheetModel.array(), 24 | })) 25 | -------------------------------------------------------------------------------- /src/test/functional/imports/expected/spreadsheet.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompletePresentation, RelatedPresentationModel } from "./index" 3 | 4 | // Helper schema for JSON fields 5 | type Literal = boolean | number | string 6 | type Json = Literal | { [key: string]: Json } | Json[] 7 | const literalSchema = z.union([z.string(), z.number(), z.boolean()]) 8 | const jsonSchema: z.ZodSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) 9 | 10 | export const SpreadsheetModel = z.object({ 11 | id: z.string(), 12 | filename: z.string(), 13 | author: z.string(), 14 | contents: jsonSchema, 15 | created: z.date(), 16 | updated: z.date(), 17 | }) 18 | 19 | export interface CompleteSpreadsheet extends z.infer { 20 | presentations: CompletePresentation[] 21 | } 22 | 23 | /** 24 | * RelatedSpreadsheetModel contains all relations on your model in addition to the scalars 25 | * 26 | * NOTE: Lazy required in case of potential circular dependencies within schema 27 | */ 28 | export const RelatedSpreadsheetModel: z.ZodSchema = z.lazy(() => SpreadsheetModel.extend({ 29 | presentations: RelatedPresentationModel.array(), 30 | })) 31 | -------------------------------------------------------------------------------- /src/test/functional/imports/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility Types 3 | */ 4 | 5 | /** 6 | * From https://github.com/sindresorhus/type-fest/ 7 | * Matches a JSON object. 8 | * This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. 9 | */ 10 | export type JsonObject = { [Key in string]?: JsonValue } 11 | 12 | /** 13 | * From https://github.com/sindresorhus/type-fest/ 14 | * Matches a JSON array. 15 | */ 16 | export interface JsonArray extends Array {} 17 | 18 | /** 19 | * From https://github.com/sindresorhus/type-fest/ 20 | * Matches any valid JSON value. 21 | */ 22 | export type JsonValue = string | number | boolean | JsonObject | JsonArray | null 23 | 24 | /** 25 | * Model Document 26 | * 27 | */ 28 | export type Document = { 29 | id: string 30 | filename: string 31 | author: string 32 | contents: string 33 | status: Status 34 | created: Date 35 | updated: Date 36 | } 37 | 38 | /** 39 | * Model Presentation 40 | * 41 | */ 42 | export type Presentation = { 43 | id: string 44 | filename: string 45 | author: string 46 | contents: string[] 47 | created: Date 48 | updated: Date 49 | } 50 | 51 | /** 52 | * Model Spreadsheet 53 | * 54 | */ 55 | export type Spreadsheet = { 56 | id: string 57 | filename: string 58 | author: string 59 | contents: JsonValue 60 | created: Date 61 | updated: Date 62 | } 63 | 64 | /** 65 | * Enums 66 | */ 67 | 68 | // Based on 69 | // https://github.com/microsoft/TypeScript/issues/3192#issuecomment-261720275 70 | 71 | export const Status = { 72 | draft: 'draft', 73 | live: 'live', 74 | archived: 'archived', 75 | } 76 | 77 | export type Status = typeof Status[keyof typeof Status] 78 | -------------------------------------------------------------------------------- /src/test/functional/imports/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | } 18 | 19 | enum Status { 20 | draft 21 | live 22 | archived 23 | } 24 | 25 | model Document { 26 | id String @id @default(cuid()) 27 | filename String @unique 28 | author String 29 | contents String 30 | status Status 31 | 32 | created DateTime @default(now()) 33 | updated DateTime @default(now()) 34 | } 35 | 36 | model Presentation { 37 | id String @id @default(cuid()) 38 | filename String @unique 39 | author String 40 | contents String[] 41 | spreadsheets Spreadsheet[] 42 | 43 | created DateTime @default(now()) 44 | updated DateTime @default(now()) 45 | } 46 | 47 | model Spreadsheet { 48 | id String @id @default(cuid()) 49 | filename String @unique 50 | author String 51 | contents Json 52 | presentations Presentation[] 53 | 54 | created DateTime @default(now()) 55 | updated DateTime @default(now()) 56 | } 57 | -------------------------------------------------------------------------------- /src/test/functional/json/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user" 2 | export * from "./post" 3 | -------------------------------------------------------------------------------- /src/test/functional/json/expected/post.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompleteUser, RelatedUserModel } from "./index" 3 | 4 | export const PostModel = z.object({ 5 | id: z.number().int(), 6 | authorId: z.number().int(), 7 | }) 8 | 9 | export interface CompletePost extends z.infer { 10 | author: CompleteUser 11 | } 12 | 13 | /** 14 | * RelatedPostModel contains all relations on your model in addition to the scalars 15 | * 16 | * NOTE: Lazy required in case of potential circular dependencies within schema 17 | */ 18 | export const RelatedPostModel: z.ZodSchema = z.lazy(() => PostModel.extend({ 19 | author: RelatedUserModel, 20 | })) 21 | -------------------------------------------------------------------------------- /src/test/functional/json/expected/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompletePost, RelatedPostModel } from "./index" 3 | 4 | // Helper schema for JSON fields 5 | type Literal = boolean | number | string 6 | type Json = Literal | { [key: string]: Json } | Json[] 7 | const literalSchema = z.union([z.string(), z.number(), z.boolean()]) 8 | const jsonSchema: z.ZodSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) 9 | 10 | export const UserModel = z.object({ 11 | id: z.number().int(), 12 | meta: jsonSchema, 13 | }) 14 | 15 | export interface CompleteUser extends z.infer { 16 | posts: CompletePost[] 17 | } 18 | 19 | /** 20 | * RelatedUserModel contains all relations on your model in addition to the scalars 21 | * 22 | * NOTE: Lazy required in case of potential circular dependencies within schema 23 | */ 24 | export const RelatedUserModel: z.ZodSchema = z.lazy(() => UserModel.extend({ 25 | posts: RelatedPostModel.array(), 26 | })) 27 | -------------------------------------------------------------------------------- /src/test/functional/json/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility Types 3 | */ 4 | 5 | /** 6 | * From https://github.com/sindresorhus/type-fest/ 7 | * Matches a JSON object. 8 | * This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. 9 | */ 10 | export type JsonObject = { [Key in string]?: JsonValue } 11 | 12 | /** 13 | * From https://github.com/sindresorhus/type-fest/ 14 | * Matches a JSON array. 15 | */ 16 | export interface JsonArray extends Array {} 17 | 18 | /** 19 | * From https://github.com/sindresorhus/type-fest/ 20 | * Matches any valid JSON value. 21 | */ 22 | export type JsonValue = string | number | boolean | JsonObject | JsonArray | null 23 | 24 | /** 25 | * Model User 26 | * 27 | */ 28 | export type User = { 29 | id: number 30 | meta: JsonValue 31 | } 32 | 33 | /** 34 | * Model Post 35 | * 36 | */ 37 | export type Post = { 38 | id: number 39 | authorId: number 40 | } 41 | -------------------------------------------------------------------------------- /src/test/functional/json/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | } 18 | 19 | model User { 20 | id Int @id @default(autoincrement()) 21 | meta Json 22 | posts Post[] 23 | } 24 | 25 | model Post { 26 | id Int @id @default(autoincrement()) 27 | authorId Int 28 | author User @relation(fields: [authorId], references: [id]) 29 | } 30 | -------------------------------------------------------------------------------- /src/test/functional/optional/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user" 2 | export * from "./post" 3 | -------------------------------------------------------------------------------- /src/test/functional/optional/expected/post.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompleteUser, RelatedUserModel } from "./index" 3 | 4 | export const PostModel = z.object({ 5 | id: z.number().int(), 6 | authorId: z.number().int(), 7 | }) 8 | 9 | export interface CompletePost extends z.infer { 10 | author?: CompleteUser | null 11 | } 12 | 13 | /** 14 | * RelatedPostModel contains all relations on your model in addition to the scalars 15 | * 16 | * NOTE: Lazy required in case of potential circular dependencies within schema 17 | */ 18 | export const RelatedPostModel: z.ZodSchema = z.lazy(() => PostModel.extend({ 19 | author: RelatedUserModel.nullish(), 20 | })) 21 | -------------------------------------------------------------------------------- /src/test/functional/optional/expected/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompletePost, RelatedPostModel } from "./index" 3 | 4 | // Helper schema for JSON fields 5 | type Literal = boolean | number | string 6 | type Json = Literal | { [key: string]: Json } | Json[] 7 | const literalSchema = z.union([z.string(), z.number(), z.boolean()]) 8 | const jsonSchema: z.ZodSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) 9 | 10 | export const UserModel = z.object({ 11 | id: z.number().int(), 12 | meta: jsonSchema, 13 | }) 14 | 15 | export interface CompleteUser extends z.infer { 16 | posts?: CompletePost | null 17 | } 18 | 19 | /** 20 | * RelatedUserModel contains all relations on your model in addition to the scalars 21 | * 22 | * NOTE: Lazy required in case of potential circular dependencies within schema 23 | */ 24 | export const RelatedUserModel: z.ZodSchema = z.lazy(() => UserModel.extend({ 25 | posts: RelatedPostModel.nullish(), 26 | })) 27 | -------------------------------------------------------------------------------- /src/test/functional/optional/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility Types 3 | */ 4 | 5 | /** 6 | * From https://github.com/sindresorhus/type-fest/ 7 | * Matches a JSON object. 8 | * This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. 9 | */ 10 | export type JsonObject = { [Key in string]?: JsonValue } 11 | 12 | /** 13 | * From https://github.com/sindresorhus/type-fest/ 14 | * Matches a JSON array. 15 | */ 16 | export interface JsonArray extends Array {} 17 | 18 | /** 19 | * From https://github.com/sindresorhus/type-fest/ 20 | * Matches any valid JSON value. 21 | */ 22 | export type JsonValue = string | number | boolean | JsonObject | JsonArray | null 23 | 24 | /** 25 | * Model User 26 | * 27 | */ 28 | export type User = { 29 | id: number 30 | meta: JsonValue 31 | } 32 | 33 | /** 34 | * Model Post 35 | * 36 | */ 37 | export type Post = { 38 | id: number 39 | authorId: number 40 | } 41 | -------------------------------------------------------------------------------- /src/test/functional/optional/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | } 18 | 19 | model User { 20 | id Int @id @default(autoincrement()) 21 | meta Json? 22 | posts Post? 23 | } 24 | 25 | model Post { 26 | id Int @id @default(autoincrement()) 27 | authorId Int @unique 28 | author User? @relation(fields: [authorId], references: [id]) 29 | } 30 | -------------------------------------------------------------------------------- /src/test/functional/recursive/expected/comment.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const CommentModel = z.object({ 4 | id: z.string(), 5 | author: z.string(), 6 | contents: z.string(), 7 | parentId: z.string(), 8 | }) 9 | 10 | export interface CompleteComment extends z.infer { 11 | parent: CompleteComment 12 | children: CompleteComment[] 13 | } 14 | 15 | /** 16 | * RelatedCommentModel contains all relations on your model in addition to the scalars 17 | * 18 | * NOTE: Lazy required in case of potential circular dependencies within schema 19 | */ 20 | export const RelatedCommentModel: z.ZodSchema = z.lazy(() => CommentModel.extend({ 21 | parent: RelatedCommentModel, 22 | children: RelatedCommentModel.array(), 23 | })) 24 | -------------------------------------------------------------------------------- /src/test/functional/recursive/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./comment" 2 | -------------------------------------------------------------------------------- /src/test/functional/recursive/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Model Comment 3 | * 4 | */ 5 | export type Comment = { 6 | id: string 7 | author: string 8 | contents: string 9 | parentId: string 10 | } 11 | -------------------------------------------------------------------------------- /src/test/functional/recursive/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | relationModel = true 18 | } 19 | 20 | model Comment { 21 | id String @id @default(uuid()) 22 | author String 23 | contents String 24 | parentId String 25 | parent Comment @relation("lineage", fields: [parentId], references: [id]) 26 | children Comment[] @relation("lineage") 27 | } 28 | -------------------------------------------------------------------------------- /src/test/functional/relation-1to1/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user" 2 | export * from "./keychain" 3 | -------------------------------------------------------------------------------- /src/test/functional/relation-1to1/expected/keychain.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompleteUser, RelatedUserModel } from "./index" 3 | 4 | export const KeychainModel = z.object({ 5 | userID: z.string(), 6 | }) 7 | 8 | export interface CompleteKeychain extends z.infer { 9 | owner: CompleteUser 10 | } 11 | 12 | /** 13 | * RelatedKeychainModel contains all relations on your model in addition to the scalars 14 | * 15 | * NOTE: Lazy required in case of potential circular dependencies within schema 16 | */ 17 | export const RelatedKeychainModel: z.ZodSchema = z.lazy(() => KeychainModel.extend({ 18 | owner: RelatedUserModel, 19 | })) 20 | -------------------------------------------------------------------------------- /src/test/functional/relation-1to1/expected/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompleteKeychain, RelatedKeychainModel } from "./index" 3 | 4 | export const UserModel = z.object({ 5 | id: z.string(), 6 | }) 7 | 8 | export interface CompleteUser extends z.infer { 9 | keychain?: CompleteKeychain | null 10 | } 11 | 12 | /** 13 | * RelatedUserModel contains all relations on your model in addition to the scalars 14 | * 15 | * NOTE: Lazy required in case of potential circular dependencies within schema 16 | */ 17 | export const RelatedUserModel: z.ZodSchema = z.lazy(() => UserModel.extend({ 18 | keychain: RelatedKeychainModel.nullish(), 19 | })) 20 | -------------------------------------------------------------------------------- /src/test/functional/relation-1to1/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | } 18 | 19 | model User { 20 | id String @id @default(uuid()) 21 | keychain Keychain? 22 | } 23 | 24 | model Keychain { 25 | userID String @id 26 | owner User @relation(fields: [userID], references: [id]) 27 | } 28 | -------------------------------------------------------------------------------- /src/test/functional/relation-false/expected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user" 2 | export * from "./post" 3 | -------------------------------------------------------------------------------- /src/test/functional/relation-false/expected/post.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const PostModel = z.object({ 4 | id: z.string(), 5 | title: z.string(), 6 | contents: z.string(), 7 | userId: z.string(), 8 | }) 9 | -------------------------------------------------------------------------------- /src/test/functional/relation-false/expected/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const UserModel = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | email: z.string(), 7 | }) 8 | -------------------------------------------------------------------------------- /src/test/functional/relation-false/prisma/.client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Model User 3 | * 4 | */ 5 | export type User = { 6 | id: string 7 | name: string 8 | email: string 9 | } 10 | 11 | /** 12 | * Model Post 13 | * 14 | */ 15 | export type Post = { 16 | id: string 17 | title: string 18 | contents: string 19 | userId: string 20 | } 21 | -------------------------------------------------------------------------------- /src/test/functional/relation-false/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = ".client" 12 | } 13 | 14 | generator zod { 15 | provider = "zod-prisma" 16 | output = "../actual/" 17 | relationModel = false 18 | } 19 | 20 | model User { 21 | id String @id @default(cuid()) 22 | name String 23 | email String 24 | posts Post[] 25 | } 26 | 27 | model Post { 28 | id String @id @default(cuid()) 29 | title String 30 | contents String 31 | author User @relation(fields: [userId], references: [id]) 32 | userId String 33 | } 34 | -------------------------------------------------------------------------------- /src/test/regressions.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { configSchema, PrismaOptions } from '../config' 3 | import { writeImportsForModel } from '../generator' 4 | import { getDMMF } from '@prisma/sdk' 5 | import { Project } from 'ts-morph' 6 | 7 | describe('Regression Tests', () => { 8 | test('#92', async () => { 9 | const config = configSchema.parse({}) 10 | const prismaOptions: PrismaOptions = { 11 | clientPath: path.resolve(__dirname, '../node_modules/@prisma/client'), 12 | outputPath: path.resolve(__dirname, './prisma/zod'), 13 | schemaPath: path.resolve(__dirname, './prisma/schema.prisma'), 14 | } 15 | 16 | const { 17 | datamodel: { 18 | models: [model], 19 | }, 20 | } = await getDMMF({ 21 | datamodel: `enum UserType { 22 | USER 23 | ADMIN 24 | } 25 | 26 | model User { 27 | id String @id 28 | type UserType 29 | }`, 30 | }) 31 | 32 | const project = new Project() 33 | const testFile = project.createSourceFile('test.ts') 34 | 35 | writeImportsForModel(model, testFile, config, prismaOptions) 36 | 37 | expect(testFile.print()).toBe( 38 | 'import * as z from "zod";\nimport { UserType } from "@prisma/client";\n' 39 | ) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/test/types.test.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import { getZodConstructor } from '../types' 3 | 4 | describe('types Package', () => { 5 | test('getZodConstructor', () => { 6 | const field: DMMF.Field = { 7 | hasDefaultValue: false, 8 | isGenerated: false, 9 | isId: false, 10 | isList: true, 11 | isRequired: false, 12 | isReadOnly: false, 13 | isUpdatedAt: false, 14 | isUnique: false, 15 | kind: 'scalar', 16 | name: 'nameList', 17 | type: 'String', 18 | documentation: ['@zod.max(64)', '@zod.min(1)'].join('\n'), 19 | } 20 | 21 | const constructor = getZodConstructor(field) 22 | 23 | expect(constructor).toBe('z.string().array().max(64).min(1).nullish()') 24 | }) 25 | 26 | test('regression - unknown type', () => { 27 | const field: DMMF.Field = { 28 | hasDefaultValue: false, 29 | isGenerated: false, 30 | isId: false, 31 | isList: false, 32 | isRequired: true, 33 | isUnique: false, 34 | isReadOnly: false, 35 | isUpdatedAt: false, 36 | kind: 'scalar', 37 | name: 'aField', 38 | type: 'SomeUnknownType', 39 | } 40 | 41 | const constructor = getZodConstructor(field) 42 | 43 | expect(constructor).toBe('z.unknown()') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/test/util.test.ts: -------------------------------------------------------------------------------- 1 | import { mock } from 'jest-mock-extended' 2 | import path from 'path' 3 | import type { CodeBlockWriter } from 'ts-morph' 4 | import { dotSlash, writeArray } from '../util' 5 | 6 | describe('Util Package', () => { 7 | test('writeArray: default newLines', () => { 8 | const arrayToWrite = ['this', 'is', 'a', 'line'] 9 | const writer = mock() 10 | 11 | writer.write.mockReturnValue(writer) 12 | 13 | writeArray(writer, arrayToWrite) 14 | 15 | expect(writer.write).toHaveBeenCalledWith('this') 16 | expect(writer.write).toHaveBeenCalledWith('is') 17 | expect(writer.write).toHaveBeenCalledWith('a') 18 | expect(writer.write).toHaveBeenCalledWith('line') 19 | expect(writer.write).toHaveBeenCalledTimes(4) 20 | 21 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(true) 22 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(true) 23 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(true) 24 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(true) 25 | expect(writer.conditionalNewLine).toHaveBeenCalledTimes(4) 26 | }) 27 | 28 | test('writeArray: no newLines', () => { 29 | const arrayToWrite = ['this', 'is', 'a', 'line'] 30 | const writer = mock() 31 | 32 | writer.write.mockReturnValue(writer) 33 | 34 | writeArray(writer, arrayToWrite, false) 35 | 36 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(false) 37 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(false) 38 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(false) 39 | expect(writer.conditionalNewLine).toHaveBeenCalledWith(false) 40 | expect(writer.conditionalNewLine).toHaveBeenCalledTimes(4) 41 | }) 42 | 43 | test('dotSlash', () => { 44 | expect(dotSlash('../banana')).toBe('../banana') 45 | expect(dotSlash('test/1/2/3')).toBe('./test/1/2/3') 46 | expect(dotSlash('../../node_modules/@prisma/client')).toBe('@prisma/client') 47 | 48 | if (path.sep !== path.posix.sep) { 49 | expect(dotSlash('test\\1\\2\\3')).toBe('./test/1/2/3') 50 | expect(dotSlash('..\\..\\node_modules\\@prisma\\client')).toBe('@prisma/client') 51 | } 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { DMMF } from '@prisma/generator-helper' 2 | import { computeCustomSchema, computeModifiers } from './docs' 3 | 4 | export const getZodConstructor = ( 5 | field: DMMF.Field, 6 | getRelatedModelName = (name: string | DMMF.SchemaEnum | DMMF.OutputType | DMMF.SchemaArg) => 7 | name.toString() 8 | ) => { 9 | let zodType = 'z.unknown()' 10 | let extraModifiers: string[] = [''] 11 | if (field.kind === 'scalar') { 12 | switch (field.type) { 13 | case 'String': 14 | zodType = 'z.string()' 15 | break 16 | case 'Int': 17 | zodType = 'z.number()' 18 | extraModifiers.push('int()') 19 | break 20 | case 'BigInt': 21 | zodType = 'z.bigint()' 22 | break 23 | case 'DateTime': 24 | zodType = 'z.date()' 25 | break 26 | case 'Float': 27 | zodType = 'z.number()' 28 | break 29 | case 'Decimal': 30 | zodType = 'z.number()' 31 | break 32 | case 'Json': 33 | zodType = 'jsonSchema' 34 | break 35 | case 'Boolean': 36 | zodType = 'z.boolean()' 37 | break 38 | // TODO: Proper type for bytes fields 39 | case 'Bytes': 40 | zodType = 'z.unknown()' 41 | break 42 | } 43 | } else if (field.kind === 'enum') { 44 | zodType = `z.nativeEnum(${field.type})` 45 | } else if (field.kind === 'object') { 46 | zodType = getRelatedModelName(field.type) 47 | } 48 | 49 | if (field.isList) extraModifiers.push('array()') 50 | if (field.documentation) { 51 | zodType = computeCustomSchema(field.documentation) ?? zodType 52 | extraModifiers.push(...computeModifiers(field.documentation)) 53 | } 54 | if (!field.isRequired && field.type !== 'Json') extraModifiers.push('nullish()') 55 | // if (field.hasDefaultValue) extraModifiers.push('optional()') 56 | 57 | return `${zodType}${extraModifiers.join('.')}` 58 | } 59 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import type { CodeBlockWriter } from 'ts-morph' 3 | import { Config } from './config' 4 | 5 | export const writeArray = (writer: CodeBlockWriter, array: string[], newLine = true) => 6 | array.forEach((line) => writer.write(line).conditionalNewLine(newLine)) 7 | 8 | export const useModelNames = ({ modelCase, modelSuffix, relationModel }: Config) => { 9 | const formatModelName = (name: string, prefix = '') => { 10 | if (modelCase === 'camelCase') { 11 | name = name.slice(0, 1).toLowerCase() + name.slice(1) 12 | } 13 | return `${prefix}${name}${modelSuffix}` 14 | } 15 | 16 | return { 17 | modelName: (name: string) => formatModelName(name, relationModel === 'default' ? '_' : ''), 18 | relatedModelName: (name: string | DMMF.SchemaEnum | DMMF.OutputType | DMMF.SchemaArg) => 19 | formatModelName( 20 | relationModel === 'default' ? name.toString() : `Related${name.toString()}` 21 | ), 22 | } 23 | } 24 | 25 | export const needsRelatedModel = (model: DMMF.Model, config: Config) => 26 | model.fields.some((field) => field.kind === 'object') && config.relationModel !== false 27 | 28 | export const chunk = (input: T, size: number): T[] => { 29 | return input.reduce((arr, item, idx) => { 30 | return idx % size === 0 31 | ? [...arr, [item]] 32 | : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]] 33 | }, []) 34 | } 35 | 36 | export const dotSlash = (input: string) => { 37 | const converted = input 38 | .replace(/^\\\\\?\\/, '') 39 | .replace(/\\/g, '/') 40 | .replace(/\/\/+/g, '/') 41 | 42 | if (converted.includes(`/node_modules/`)) return converted.split(`/node_modules/`).slice(-1)[0] 43 | 44 | if (converted.startsWith(`../`)) return converted 45 | 46 | return './' + converted 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // stricter type-checking for stronger correctness. Recommended by TS 11 | "strict": true, 12 | "rootDir": "./src", 13 | // linter checks for common issues 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | // use Node's module resolution algorithm, instead of the legacy TS one 20 | "moduleResolution": "node", 21 | // interop between ESM and CJS modules. Recommended by TS 22 | "esModuleInterop": true, 23 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 24 | "skipLibCheck": true, 25 | // error out if import and file system have a casing mismatch. Recommended by TS 26 | "forceConsistentCasingInFileNames": true, 27 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 28 | "noEmit": true, 29 | "downlevelIteration": true, 30 | "resolveJsonModule": true, 31 | "noErrorTruncation": true 32 | } 33 | } 34 | --------------------------------------------------------------------------------