├── .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 |
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 | -
55 | About The Project
56 |
59 |
60 | -
61 | Getting Started
62 |
66 |
67 | - Usage
68 |
79 |
80 | - Examples
81 | - Roadmap
82 | - Contributing
83 | - License
84 | - Contact
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------