zod4-prisma 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
package/README.md ADDED
@@ -0,0 +1,304 @@
1
+ <!-- PROJECT SHIELDS -->
2
+ <!--
3
+ *** I'm using markdown "reference style" links for readability.
4
+ *** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
5
+ *** See the bottom of this document for the declaration of the reference variables
6
+ *** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
7
+ *** https://www.markdownguide.org/basic-syntax/#reference-style-links
8
+ -->
9
+
10
+ [![NPM][npm-shield]][npm-url]
11
+ [![Contributors][contributors-shield]][contributors-url]
12
+ [![Forks][forks-shield]][forks-url]
13
+ [![Stargazers][stars-shield]][stars-url]
14
+ [![Issues][issues-shield]][issues-url]
15
+ [![MIT License][license-shield]][license-url]
16
+
17
+ <!-- PROJECT LOGO -->
18
+ <br />
19
+ <p align="center">
20
+ <a href="https://github.com/nestifine/zod4-prisma">
21
+ <img src="https://raw.githubusercontent.com/nestifine/zod4-prisma/main/images/zod-prisma.svg" alt="Logo" width="120" height="120">
22
+ </a>
23
+ <h3 align="center">zod4-prisma</h3>
24
+ <p align="center">
25
+ A Prisma generator that creates Zod v4 schemas for all of your models.
26
+ <br />
27
+ <a href="https://github.com/nestifine/zod4-prisma"><strong>Explore the docs »</strong></a>
28
+ <br />
29
+ <br />
30
+ <a href="https://github.com/nestifine/zod4-prisma/blob/main/src/test/functional">View Demo</a>
31
+ ·
32
+ <a href="https://github.com/nestifine/zod4-prisma/issues">Report Bug</a>
33
+ ·
34
+ <a href="https://github.com/nestifine/zod4-prisma/issues">Request Feature</a>
35
+ </p>
36
+ </p>
37
+
38
+ <!-- TABLE OF CONTENTS -->
39
+ <details open="open">
40
+ <summary><h2 style="display: inline-block">Table of Contents</h2></summary>
41
+ <ol>
42
+ <li>
43
+ <a href="#about-the-project">About The Project</a>
44
+ <ul>
45
+ <li><a href="#built-with">Built With</a></li>
46
+ </ul>
47
+ </li>
48
+ <li>
49
+ <a href="#getting-started">Getting Started</a>
50
+ <ul>
51
+ <li><a href="#prerequisites">Prerequisites</a></li>
52
+ <li><a href="#installation">Installation</a></li>
53
+ </ul>
54
+ </li>
55
+ <li><a href="#usage">Usage</a>
56
+ <ul>
57
+ <li><a href="#jsdoc-generation">JSDoc Generation</a></li>
58
+ <li><a href="#extending-zod-fields">Extending Zod Fields</a></li>
59
+ <li>
60
+ <a href="#importing-helpers">Importing Helpers</a>
61
+ <ul>
62
+ <li><a href="#custom-zod-schema">Custom Zod Schemas</a></li>
63
+ </ul>
64
+ </li>
65
+ <li><a href="#json-fields">JSON Fields</a></li>
66
+ </ul>
67
+ </li>
68
+ <li><a href="#examples">Examples</a></li>
69
+ <li><a href="#roadmap">Roadmap</a></li>
70
+ <li><a href="#contributing">Contributing</a></li>
71
+ <li><a href="#license">License</a></li>
72
+ <li><a href="#contact">Contact</a></li>
73
+ </ol>
74
+ </details>
75
+
76
+ <!-- ABOUT THE PROJECT -->
77
+
78
+ ## About The Project
79
+
80
+ I got tired of having to manually create Zod schemas for my Prisma models and of updating them everytime I made schema changes.
81
+ This provides a way of automatically generating them with your prisma
82
+
83
+ <!-- [![Product Name Screen Shot][product-screenshot]](https://example.com) -->
84
+
85
+ ### Built With
86
+
87
+ - [tsup](https://github.com/egoist/tsup)
88
+ - [Zod](https://github.com/colinhacks/zod)
89
+ - [ts-morph](https://github.com/dsherret/ts-morph)
90
+ - [Based on this gist](https://gist.github.com/deckchairlabs/8a11c33311c01273deec7e739417dbc9)
91
+
92
+ <!-- GETTING STARTED -->
93
+
94
+ ## Getting Started
95
+
96
+ To get a local copy up and running follow these simple steps.
97
+
98
+ ### Prerequisites
99
+
100
+ This project uses npm.
101
+
102
+ ### Installation
103
+
104
+ 0. **Ensure your tsconfig.json enables the compiler's strict mode.**
105
+ **Zod requires it and so do we, you will experience TS errors without strict mode enabled**
106
+
107
+ 1. Add zod4-prisma as a dev dependency
108
+
109
+ ```sh
110
+ npm install --save-dev zod4-prisma
111
+ ```
112
+
113
+ 2. Add the zod4-prisma generator to your schema.prisma
114
+
115
+ ```prisma
116
+ generator zod {
117
+ provider = "zod4-prisma"
118
+ output = "./zod" // (default) the directory where generated zod schemas will be saved
119
+
120
+ relationModel = true // (default) Create and export both plain and related models.
121
+ // relationModel = "default" // Do not export model without relations.
122
+ // relationModel = false // Do not generate related model
123
+
124
+ modelCase = "PascalCase" // (default) Output models using pascal case (ex. UserModel, PostModel)
125
+ // modelCase = "camelCase" // Output models using camel case (ex. userModel, postModel)
126
+
127
+ modelSuffix = "Model" // (default) Suffix to apply to your prisma models when naming Zod schemas
128
+
129
+ // useDecimalJs = false // (default) represent the prisma Decimal type using as a JS number
130
+ useDecimalJs = true // represent the prisma Decimal type using Decimal.js (as Prisma does)
131
+
132
+ imports = null // (default) will import the referenced file in generated schemas to be used via imports.someExportedVariable
133
+
134
+ // https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-by-null-values
135
+ prismaJsonNullability = true // (default) uses prisma's scheme for JSON field nullability
136
+ // prismaJsonNullability = false // allows null assignment to optional JSON fields
137
+ }
138
+ ```
139
+
140
+ 3. Run `npx prisma generate` to generate your zod schemas
141
+ 4. Import the generated schemas from your selected output location
142
+
143
+ <!-- USAGE EXAMPLES -->
144
+
145
+ ## Usage
146
+
147
+ ### JSDoc Generation
148
+
149
+ [Rich-comments](https://www.prisma.io/docs/concepts/components/prisma-schema#comments)
150
+ in the Prisma schema will be transformed into JSDoc for the associated fields:
151
+
152
+ > _Note: make sure to use a triple-slash. Double-slash comments won't be processed._
153
+
154
+ ```prisma
155
+ model Post {
156
+ /// The unique identifier for the post
157
+ /// @default {Generated by database}
158
+ id String @id @default(uuid())
159
+
160
+ /// A brief title that describes the contents of the post
161
+ title String
162
+
163
+ /// The actual contents of the post.
164
+ contents String
165
+ }
166
+ ```
167
+
168
+ Generated code:
169
+
170
+ ```ts
171
+ export const PostModel = z.object({
172
+ /**
173
+ * The unique identifier for the post
174
+ * @default {Generated by database}
175
+ */
176
+ id: z.string().uuid(),
177
+ /**
178
+ * A brief title that describes the contents of the post
179
+ */
180
+ title: z.string(),
181
+ /**
182
+ * The actual contents of the post.
183
+ */
184
+ contents: z.string(),
185
+ })
186
+ ```
187
+
188
+ ### Extending Zod Fields
189
+
190
+ You can also use the `@zod` keyword in rich-comments in the Prisma schema
191
+ to extend your Zod schema fields:
192
+
193
+ ```prisma
194
+ model Post {
195
+ id String @id @default(uuid()) /// @zod.uuid()
196
+
197
+ /// @zod.max(255, { message: "The title must be shorter than 256 characters" })
198
+ title String
199
+
200
+ contents String /// @zod.max(10240)
201
+ }
202
+ ```
203
+
204
+ Generated code:
205
+
206
+ ```ts
207
+ export const PostModel = z.object({
208
+ id: z.string().uuid(),
209
+ title: z.string().max(255, { message: 'The title must be shorter than 256 characters' }),
210
+ contents: z.string().max(10240),
211
+ })
212
+ ```
213
+
214
+ ### Importing Helpers
215
+
216
+ Sometimes its useful to define a custom Zod preprocessor or transformer for your data.
217
+ zod4-prisma enables you to reuse these by importing them via a config options. For example:
218
+
219
+ ```prisma
220
+ generator zod {
221
+ provider = "zod4-prisma"
222
+ output = "./zod"
223
+ imports = "../src/zod-schemas"
224
+ }
225
+
226
+ model User {
227
+ username String /// @zod.refine(imports.isValidUsername)
228
+ }
229
+ ```
230
+
231
+ The referenced file can then be used by simply referring to exported members via `imports.whateverExport`.
232
+ The generated zod schema files will now include a namespaced import like the following.
233
+
234
+ ```typescript
235
+ import * as imports from '../../src/zod-schemas'
236
+ ```
237
+
238
+ #### Custom Zod Schema
239
+
240
+ In conjunction with this import option, you may want to utilize an entirely custom zod schema for a field.
241
+ This can be accomplished by using the special comment directive `@zod.custom()`.
242
+ By specifying the custom schema within the parentheses you can replace the autogenerated type that would normally be assigned to the field.
243
+
244
+ > For instance if you wanted to use `z.preprocess`
245
+
246
+ ### JSON Fields
247
+
248
+ JSON fields in Prisma disallow null values. This is to disambiguate between setting a field's value to NULL in the database and having
249
+ a value of null stored in the JSON. In accordance with this zod4-prisma will default to disallowing null values, even if your JSON field is optional.
250
+
251
+ If you would like to revert this behavior and allow null assignment to JSON fields,
252
+ you can set `prismaJsonNullability` to `false` in the generator options.
253
+
254
+ ## Examples
255
+
256
+ <!-- Use this space to show useful examples of how a project can be used. Additional screenshots, code examples and demos work well in this space. You may also link to more resources. -->
257
+
258
+ _For examples, please refer to the [Examples Directory](https://github.com/nestifine/zod4-prisma/blob/main/examples) or the [Functional Tests](https://github.com/nestifine/zod4-prisma/blob/main/src/test/functional)_
259
+
260
+ <!-- ROADMAP -->
261
+
262
+ ## Roadmap
263
+
264
+ See the [open issues](https://github.com/nestifine/zod4-prisma/issues) for a list of proposed features (and known issues).
265
+
266
+ <!-- CONTRIBUTING -->
267
+
268
+ ## Contributing
269
+
270
+ 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**.
271
+
272
+ 1. Fork the Project
273
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
274
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
275
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
276
+ 5. Open a Pull Request
277
+
278
+ <!-- LICENSE -->
279
+
280
+ ## License
281
+
282
+ Distributed under the MIT License. See `LICENSE` for more information.
283
+
284
+ <!-- CONTACT -->
285
+
286
+ ## Contact
287
+
288
+ Project Link: [https://github.com/nestifine/zod4-prisma](https://github.com/nestifine/zod4-prisma)
289
+
290
+ <!-- MARKDOWN LINKS & IMAGES -->
291
+ <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
292
+
293
+ [npm-shield]: https://img.shields.io/npm/v/zod4-prisma?style=for-the-badge
294
+ [npm-url]: https://www.npmjs.com/package/zod4-prisma
295
+ [contributors-shield]: https://img.shields.io/github/contributors/nestifine/zod4-prisma.svg?style=for-the-badge
296
+ [contributors-url]: https://github.com/nestifine/zod4-prisma/graphs/contributors
297
+ [forks-shield]: https://img.shields.io/github/forks/nestifine/zod4-prisma.svg?style=for-the-badge
298
+ [forks-url]: https://github.com/nestifine/zod4-prisma/network/members
299
+ [stars-shield]: https://img.shields.io/github/stars/nestifine/zod4-prisma.svg?style=for-the-badge
300
+ [stars-url]: https://github.com/nestifine/zod4-prisma/stargazers
301
+ [issues-shield]: https://img.shields.io/github/issues/nestifine/zod4-prisma.svg?style=for-the-badge
302
+ [issues-url]: https://github.com/nestifine/zod4-prisma/issues
303
+ [license-shield]: https://img.shields.io/github/license/nestifine/zod4-prisma.svg?style=for-the-badge
304
+ [license-url]: https://github.com/nestifine/zod4-prisma/blob/main/LICENSE
package/bin/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../dist/index')
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,390 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // package.json
26
+ var version = "1.0.0";
27
+
28
+ // src/index.ts
29
+ var import_generator_helper = require("@prisma/generator-helper");
30
+ var import_typescript = require("typescript");
31
+
32
+ // src/config.ts
33
+ var import_zod = require("zod");
34
+ var stringBoolean = import_zod.z.enum(["true", "false"]);
35
+ var configBoolean = stringBoolean.default("true").transform((arg) => arg === "true");
36
+ var configSchema = import_zod.z.object({
37
+ relationModel: import_zod.z.union([import_zod.z.literal("default"), configBoolean]),
38
+ modelSuffix: import_zod.z.string().default("Model"),
39
+ modelCase: import_zod.z.enum(["PascalCase", "camelCase"]).default("PascalCase"),
40
+ useDecimalJs: configBoolean,
41
+ imports: import_zod.z.string().optional(),
42
+ prismaJsonNullability: configBoolean
43
+ });
44
+
45
+ // src/generator.ts
46
+ var import_path = __toESM(require("path"));
47
+ var import_ts_morph = require("ts-morph");
48
+
49
+ // src/util.ts
50
+ var writeArray = (writer, array, newLine = true) => array.forEach((line) => {
51
+ writer.write(line).conditionalNewLine(newLine);
52
+ });
53
+ var useModelNames = ({ modelCase, modelSuffix, relationModel }) => {
54
+ const formatModelName = (name, prefix = "") => {
55
+ if (modelCase === "camelCase") {
56
+ name = name.slice(0, 1).toLowerCase() + name.slice(1);
57
+ }
58
+ return `${prefix}${name}${modelSuffix}`;
59
+ };
60
+ return {
61
+ modelName: (name) => formatModelName(name, relationModel === "default" ? "_" : ""),
62
+ relatedModelName: (name) => formatModelName(
63
+ relationModel === "default" ? name.toString() : `Related${name.toString()}`
64
+ )
65
+ };
66
+ };
67
+ var needsRelatedModel = (model, config) => model.fields.some((field) => field.kind === "object") && config.relationModel !== false;
68
+ var chunk = (input, size) => {
69
+ return input.reduce((arr, item, idx) => {
70
+ return idx % size === 0 ? [...arr, [item]] : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];
71
+ }, []);
72
+ };
73
+ var dotSlash = (input) => {
74
+ const converted = input.replace(/^\\\\\?\\/, "").replace(/\\/g, "/").replace(/\/\/+/g, "/");
75
+ if (converted.includes(`/node_modules/`)) {
76
+ const p = converted.split(`/node_modules/`).slice(-1)[0];
77
+ return p.startsWith(".prisma/") ? "@prisma/client" : p;
78
+ }
79
+ if (converted.startsWith(`../`)) return converted;
80
+ return "./" + converted;
81
+ };
82
+
83
+ // src/docs.ts
84
+ var import_parenthesis = require("parenthesis");
85
+ var getJSDocs = (docString) => {
86
+ const lines = [];
87
+ if (docString) {
88
+ const docLines = docString.split("\n").filter((dL) => !dL.trimStart().startsWith("@zod"));
89
+ if (docLines.length) {
90
+ lines.push("/**");
91
+ docLines.forEach((dL) => {
92
+ lines.push(` * ${dL}`);
93
+ });
94
+ lines.push(" */");
95
+ }
96
+ }
97
+ return lines;
98
+ };
99
+ var getZodDocElements = (docString) => docString.split("\n").filter((line) => line.trimStart().startsWith("@zod")).map((line) => line.trimStart().slice(4)).flatMap(
100
+ (line) => (
101
+ // Array.from(line.matchAll(/\.([^().]+\(.*?\))/g), (m) => m.slice(1)).flat()
102
+ chunk((0, import_parenthesis.parse)(line), 2).slice(0, -1).map(
103
+ ([each, contents]) => each.replace(/\)?\./, "") + `${(0, import_parenthesis.stringify)(contents)})`
104
+ )
105
+ )
106
+ );
107
+ var computeCustomSchema = (docString) => {
108
+ return getZodDocElements(docString).find((modifier) => modifier.startsWith("custom("))?.slice(7).slice(0, -1);
109
+ };
110
+ var computeModifiers = (docString) => {
111
+ return getZodDocElements(docString).filter((each) => !each.startsWith("custom("));
112
+ };
113
+
114
+ // src/types.ts
115
+ var getZodConstructor = (field, getRelatedModelName = (name) => name.toString()) => {
116
+ let zodType = "z.unknown()";
117
+ let extraModifiers = [""];
118
+ if (field.kind === "scalar") {
119
+ switch (field.type) {
120
+ case "String":
121
+ zodType = "z.string()";
122
+ break;
123
+ case "Int":
124
+ zodType = "z.number()";
125
+ extraModifiers.push("int()");
126
+ break;
127
+ case "BigInt":
128
+ zodType = "z.bigint()";
129
+ break;
130
+ case "DateTime":
131
+ zodType = "z.date()";
132
+ break;
133
+ case "Float":
134
+ zodType = "z.number()";
135
+ break;
136
+ case "Decimal":
137
+ zodType = "z.number()";
138
+ break;
139
+ case "Json":
140
+ zodType = "jsonSchema";
141
+ break;
142
+ case "Boolean":
143
+ zodType = "z.boolean()";
144
+ break;
145
+ // TODO: Proper type for bytes fields
146
+ case "Bytes":
147
+ zodType = "z.unknown()";
148
+ break;
149
+ }
150
+ } else if (field.kind === "enum") {
151
+ zodType = `z.nativeEnum(${field.type})`;
152
+ } else if (field.kind === "object") {
153
+ zodType = getRelatedModelName(field.type);
154
+ }
155
+ if (field.isList) extraModifiers.push("array()");
156
+ if (field.documentation) {
157
+ zodType = computeCustomSchema(field.documentation) ?? zodType;
158
+ extraModifiers.push(...computeModifiers(field.documentation));
159
+ }
160
+ if (!field.isRequired && field.type !== "Json") extraModifiers.push("nullish()");
161
+ return `${zodType}${extraModifiers.join(".")}`;
162
+ };
163
+
164
+ // src/generator.ts
165
+ var writeImportsForModel = (model, sourceFile, config, { schemaPath, outputPath, clientPath }) => {
166
+ const { relatedModelName } = useModelNames(config);
167
+ const importList = [
168
+ {
169
+ kind: import_ts_morph.StructureKind.ImportDeclaration,
170
+ namespaceImport: "z",
171
+ moduleSpecifier: "zod"
172
+ }
173
+ ];
174
+ if (config.imports) {
175
+ importList.push({
176
+ kind: import_ts_morph.StructureKind.ImportDeclaration,
177
+ namespaceImport: "imports",
178
+ moduleSpecifier: dotSlash(
179
+ import_path.default.relative(outputPath, import_path.default.resolve(import_path.default.dirname(schemaPath), config.imports))
180
+ )
181
+ });
182
+ }
183
+ const hasNonCustomDecimalFieldForImports = model.fields.some(
184
+ (f) => f.type === "Decimal" && (!f.documentation || !computeCustomSchema(f.documentation))
185
+ );
186
+ if (config.useDecimalJs && hasNonCustomDecimalFieldForImports) {
187
+ importList.push({
188
+ kind: import_ts_morph.StructureKind.ImportDeclaration,
189
+ namedImports: ["Decimal"],
190
+ moduleSpecifier: "decimal.js"
191
+ });
192
+ }
193
+ const enumFields = model.fields.filter((f) => f.kind === "enum");
194
+ const relationFields = model.fields.filter((f) => f.kind === "object");
195
+ const relativePath = import_path.default.relative(outputPath, clientPath);
196
+ if (enumFields.length > 0) {
197
+ importList.push({
198
+ kind: import_ts_morph.StructureKind.ImportDeclaration,
199
+ isTypeOnly: enumFields.length === 0,
200
+ moduleSpecifier: dotSlash(relativePath),
201
+ namedImports: enumFields.map((f) => f.type)
202
+ });
203
+ }
204
+ if (config.relationModel !== false && relationFields.length > 0) {
205
+ const filteredFields = relationFields.filter((f) => f.type !== model.name);
206
+ if (filteredFields.length > 0) {
207
+ importList.push({
208
+ kind: import_ts_morph.StructureKind.ImportDeclaration,
209
+ moduleSpecifier: "./index",
210
+ namedImports: Array.from(
211
+ new Set(filteredFields.flatMap((f) => [`Complete${f.type}`, relatedModelName(f.type)]))
212
+ )
213
+ });
214
+ }
215
+ }
216
+ sourceFile.addImportDeclarations(importList);
217
+ };
218
+ var writeTypeSpecificSchemas = (model, sourceFile, config, _prismaOptions) => {
219
+ if (model.fields.some((f) => f.type === "Json")) {
220
+ sourceFile.addStatements((writer) => {
221
+ writer.newLine();
222
+ writeArray(writer, [
223
+ "// Helper schema for JSON fields",
224
+ `type Literal = boolean | number | string${config.prismaJsonNullability ? "" : "| null"}`,
225
+ "type Json = Literal | { [key: string]: Json } | Json[]",
226
+ `const literalSchema = z.union([z.string(), z.number(), z.boolean()${config.prismaJsonNullability ? "" : ", z.null()"}])`,
227
+ "const jsonSchema: z.ZodSchema<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(z.string(), jsonSchema)]))"
228
+ ]);
229
+ });
230
+ }
231
+ const hasNonCustomDecimalField = model.fields.some(
232
+ (f) => f.type === "Decimal" && (!f.documentation || !computeCustomSchema(f.documentation))
233
+ );
234
+ if (config.useDecimalJs && hasNonCustomDecimalField) {
235
+ sourceFile.addStatements((writer) => {
236
+ writer.newLine();
237
+ writeArray(writer, [
238
+ "// Helper schema for Decimal fields",
239
+ "z",
240
+ ".instanceof(Decimal)",
241
+ ".or(z.string())",
242
+ ".or(z.number())",
243
+ ".refine((value) => {",
244
+ " try {",
245
+ " return new Decimal(value);",
246
+ " } catch (error) {",
247
+ " return false;",
248
+ " }",
249
+ "})",
250
+ ".transform((value) => new Decimal(value));"
251
+ ]);
252
+ });
253
+ }
254
+ };
255
+ var generateSchemaForModel = (model, sourceFile, config, _prismaOptions) => {
256
+ const { modelName } = useModelNames(config);
257
+ sourceFile.addVariableStatement({
258
+ declarationKind: import_ts_morph.VariableDeclarationKind.Const,
259
+ isExported: true,
260
+ leadingTrivia: (writer) => writer.blankLineIfLastNot(),
261
+ declarations: [
262
+ {
263
+ name: modelName(model.name),
264
+ initializer(writer) {
265
+ writer.write("z.object(").inlineBlock(() => {
266
+ model.fields.filter((f) => f.kind !== "object").forEach((field) => {
267
+ writeArray(writer, getJSDocs(field.documentation));
268
+ writer.write(`${field.name}: ${getZodConstructor(field)}`).write(",").newLine();
269
+ });
270
+ }).write(")");
271
+ }
272
+ }
273
+ ]
274
+ });
275
+ };
276
+ var generateRelatedSchemaForModel = (model, sourceFile, config, _prismaOptions) => {
277
+ const { modelName, relatedModelName } = useModelNames(config);
278
+ const relationFields = model.fields.filter((f) => f.kind === "object");
279
+ sourceFile.addInterface({
280
+ name: `Complete${model.name}`,
281
+ isExported: true,
282
+ extends: [`z.infer<typeof ${modelName(model.name)}>`],
283
+ properties: relationFields.map((f) => ({
284
+ hasQuestionToken: !f.isRequired,
285
+ name: f.name,
286
+ type: `Complete${f.type}${f.isList ? "[]" : ""}${!f.isRequired ? " | null" : ""}`
287
+ }))
288
+ });
289
+ sourceFile.addStatements(
290
+ (writer) => writeArray(writer, [
291
+ "",
292
+ "/**",
293
+ ` * ${relatedModelName(
294
+ model.name
295
+ )} contains all relations on your model in addition to the scalars`,
296
+ " *",
297
+ " * NOTE: Lazy required in case of potential circular dependencies within schema",
298
+ " */"
299
+ ])
300
+ );
301
+ sourceFile.addVariableStatement({
302
+ declarationKind: import_ts_morph.VariableDeclarationKind.Const,
303
+ isExported: true,
304
+ declarations: [
305
+ {
306
+ name: relatedModelName(model.name),
307
+ type: `z.ZodSchema<Complete${model.name}>`,
308
+ initializer(writer) {
309
+ writer.write(`z.lazy(() => ${modelName(model.name)}.extend(`).inlineBlock(() => {
310
+ relationFields.forEach((field) => {
311
+ writeArray(writer, getJSDocs(field.documentation));
312
+ writer.write(`${field.name}: ${getZodConstructor(field, relatedModelName)}`).write(",").newLine();
313
+ });
314
+ }).write("))");
315
+ }
316
+ }
317
+ ]
318
+ });
319
+ };
320
+ var populateModelFile = (model, sourceFile, config, prismaOptions) => {
321
+ writeImportsForModel(model, sourceFile, config, prismaOptions);
322
+ writeTypeSpecificSchemas(model, sourceFile, config, prismaOptions);
323
+ generateSchemaForModel(model, sourceFile, config, prismaOptions);
324
+ if (needsRelatedModel(model, config))
325
+ generateRelatedSchemaForModel(model, sourceFile, config, prismaOptions);
326
+ };
327
+ var generateBarrelFile = (models, indexFile) => {
328
+ models.forEach((model) => {
329
+ indexFile.addExportDeclaration({
330
+ moduleSpecifier: `./${model.name.toLowerCase()}`
331
+ });
332
+ });
333
+ };
334
+
335
+ // src/index.ts
336
+ var import_ts_morph2 = require("ts-morph");
337
+ (0, import_generator_helper.generatorHandler)({
338
+ onManifest() {
339
+ return {
340
+ version,
341
+ prettyName: "Zod Schemas",
342
+ defaultOutput: "zod"
343
+ };
344
+ },
345
+ onGenerate(options) {
346
+ const project = new import_ts_morph2.Project();
347
+ const models = options.dmmf.datamodel.models;
348
+ const { schemaPath } = options;
349
+ const outputPath = options.generator.output.value;
350
+ const clientPath = options.otherGenerators.find(
351
+ (each) => each.provider.value === "prisma-client" || each.provider.value === "prisma-client-js"
352
+ ).output.value;
353
+ const results = configSchema.safeParse(options.generator.config);
354
+ if (!results.success)
355
+ throw new Error(
356
+ "Incorrect config provided. Please check the values you provided and try again."
357
+ );
358
+ const config = results.data;
359
+ const prismaOptions = {
360
+ clientPath,
361
+ outputPath,
362
+ schemaPath
363
+ };
364
+ const indexFile = project.createSourceFile(
365
+ `${outputPath}/index.ts`,
366
+ {},
367
+ { overwrite: true }
368
+ );
369
+ generateBarrelFile(models, indexFile);
370
+ indexFile.formatText({
371
+ indentSize: 2,
372
+ convertTabsToSpaces: true,
373
+ semicolons: import_typescript.SemicolonPreference.Remove
374
+ });
375
+ models.forEach((model) => {
376
+ const sourceFile = project.createSourceFile(
377
+ `${outputPath}/${model.name.toLowerCase()}.ts`,
378
+ {},
379
+ { overwrite: true }
380
+ );
381
+ populateModelFile(model, sourceFile, config, prismaOptions);
382
+ sourceFile.formatText({
383
+ indentSize: 2,
384
+ convertTabsToSpaces: true,
385
+ semicolons: import_typescript.SemicolonPreference.Remove
386
+ });
387
+ });
388
+ return project.save();
389
+ }
390
+ });
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "zod4-prisma",
3
+ "version": "1.0.0",
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/nestifine/zod4-prisma#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/nestifine/zod4-prisma.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/nestifine/zod4-prisma/issues"
14
+ },
15
+ "main": "dist/index.js",
16
+ "typings": "dist/index.d.ts",
17
+ "bin": "bin/cli.js",
18
+ "keywords": [
19
+ "zod",
20
+ "prisma",
21
+ "generator"
22
+ ],
23
+ "files": [
24
+ "bin",
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup src/index.ts --format cjs --dts --clean --out-dir dist --target node18 --external @prisma/generator-helper --external ts-morph --external parenthesis --external typescript",
29
+ "lint": "tsc --noEmit",
30
+ "prepublishOnly": "tsup src/index.ts --format cjs --dts --clean --out-dir dist --target node18 --external @prisma/generator-helper --external ts-morph --external parenthesis --external typescript",
31
+ "test": "npx jest --forceExit",
32
+ "prepare": "test -d .git && lefthook install || true"
33
+ },
34
+ "prettier": {
35
+ "printWidth": 100,
36
+ "semi": false,
37
+ "singleQuote": true,
38
+ "tabWidth": 4,
39
+ "trailingComma": "es5",
40
+ "useTabs": true
41
+ },
42
+ "eslintConfig": {
43
+ "rules": {
44
+ "react-hooks/rules-of-hooks": "off"
45
+ }
46
+ },
47
+ "jest": {
48
+ "testEnvironment": "node",
49
+ "transform": {
50
+ "^.+\\.tsx?$": [
51
+ "ts-jest",
52
+ {
53
+ "tsconfig": "tsconfig.json"
54
+ }
55
+ ]
56
+ },
57
+ "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
58
+ "moduleFileExtensions": [
59
+ "ts",
60
+ "tsx",
61
+ "js",
62
+ "jsx",
63
+ "json",
64
+ "node"
65
+ ]
66
+ },
67
+ "dependencies": {
68
+ "@prisma/generator-helper": "^7.6.0",
69
+ "parenthesis": "^3.1.8",
70
+ "ts-morph": "^27.0.2"
71
+ },
72
+ "devDependencies": {
73
+ "@biomejs/biome": "^2.4.10",
74
+ "@prisma/client": "^7.6.0",
75
+ "@prisma/internals": "^7.6.0",
76
+ "@tsconfig/recommended": "^1.0.1",
77
+ "@types/fs-extra": "^11.0.4",
78
+ "@types/jest": "^30.0.0",
79
+ "@types/node": "^25.5.2",
80
+ "decimal.js": "^10.6.0",
81
+ "fast-glob": "^3.3.3",
82
+ "fs-extra": "^11.3.4",
83
+ "jest-mock-extended": "^4.0.0",
84
+ "lefthook": "^2.1.4",
85
+ "prisma": "^7.6.0",
86
+ "resolve-from": "^5.0.0",
87
+ "ts-jest": "^29.4.9",
88
+ "tslib": "^2.8.1",
89
+ "tsup": "^8.5.1",
90
+ "typescript": "^5.6.3",
91
+ "zod": "^4.3.6"
92
+ },
93
+ "peerDependencies": {
94
+ "decimal.js": "^10.0.0",
95
+ "prisma": "^7.0.0",
96
+ "typescript": ">=4.5.0",
97
+ "zod": "^4.0.0"
98
+ },
99
+ "peerDependenciesMeta": {
100
+ "decimal.js": {
101
+ "optional": true
102
+ },
103
+ "typescript": {
104
+ "optional": true
105
+ }
106
+ },
107
+ "engines": {
108
+ "node": ">=18"
109
+ }
110
+ }