prisma-arktype 2.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) 2025 Ibrahim Saberi
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,345 @@
1
+ # prisma-arktype
2
+
3
+ Generate [ArkType](https://arktype.io) validation schemas from your [Prisma](https://www.prisma.io) schema.
4
+
5
+ This package is heavily inspired by and based on the structure of [prismabox](https://github.com/m1212e/prismabox), which generates TypeBox schemas from Prisma schemas.
6
+
7
+ ## Features
8
+
9
+ - 🎯 **Type-safe validation** - Generate ArkType schemas that match your Prisma models
10
+ - 🔄 **Automatic generation** - Schemas are generated automatically when you run `prisma generate`
11
+ - 📦 **Comprehensive coverage** - Generates schemas for models, relations, where clauses, select, include, orderBy, and more
12
+ - 🎨 **Customizable** - Control schema generation with annotations
13
+ - 🚀 **Zero config** - Works out of the box with sensible defaults
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install prisma-arktype arktype
19
+ # or
20
+ pnpm add prisma-arktype arktype
21
+ # or
22
+ yarn add prisma-arktype arktype
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Basic Setup
28
+
29
+ Add the generator to your `schema.prisma` file:
30
+
31
+ ```prisma
32
+ generator prisma-arktype {
33
+ provider = "prisma-arktype"
34
+ output = "./generated/validators"
35
+ }
36
+
37
+ model User {
38
+ id String @id @default(cuid())
39
+ email String @unique
40
+ name String?
41
+ posts Post[]
42
+ createdAt DateTime @default(now())
43
+ updatedAt DateTime @updatedAt
44
+ }
45
+
46
+ model Post {
47
+ id String @id @default(cuid())
48
+ title String
49
+ content String?
50
+ published Boolean @default(false)
51
+ author User @relation(fields: [authorId], references: [id])
52
+ authorId String
53
+ createdAt DateTime @default(now())
54
+ updatedAt DateTime @updatedAt
55
+ }
56
+ ```
57
+
58
+ Then run:
59
+
60
+ ```bash
61
+ npx prisma generate
62
+ ```
63
+
64
+ ### Configuration Options
65
+
66
+ Configure the generator in your `schema.prisma`:
67
+
68
+ ```prisma
69
+ generator prisma-arktype {
70
+ provider = "prisma-arktype"
71
+ output = "./generated/validators"
72
+ arktypeImportDependencyName = "arktype"
73
+ ignoredKeysOnInputModels = ["id", "createdAt", "updatedAt"]
74
+ }
75
+ ```
76
+
77
+ #### Configuration Options
78
+
79
+ | Option | Type | Default | Description |
80
+ |--------|------|---------|-------------|
81
+ | `output` | `string` | `"./prisma/generated/validators"` | Output directory for generated schemas |
82
+ | `arktypeImportDependencyName` | `string` | `"arktype"` | The package name to import from |
83
+ | `ignoredKeysOnInputModels` | `string[]` | `["id", "createdAt", "updatedAt"]` | Fields to exclude from input models |
84
+
85
+ ### Generated Schemas
86
+
87
+ For each model, the generator creates multiple schema types:
88
+
89
+ - **`ModelPlain`** - Fields without relationships
90
+ - **`ModelRelations`** - Relationship definitions only
91
+ - **`Model`** - Complete composite schema (Plain & Relations)
92
+ - **`ModelWhere`** - Where clause schema
93
+ - **`ModelWhereUnique`** - Unique where clause schema
94
+ - **`ModelCreate`** - Create input schema
95
+ - **`ModelUpdate`** - Update input schema
96
+ - **`ModelSelect`** - Select schema
97
+ - **`ModelInclude`** - Include schema
98
+ - **`ModelOrderBy`** - OrderBy schema
99
+
100
+ ### Using Generated Schemas
101
+
102
+ ```typescript
103
+ import { type } from "arktype";
104
+ import { User, UserCreate, UserWhere } from "./generated/validators";
105
+
106
+ // Validate a user object
107
+ const userResult = User(someUserData);
108
+ if (userResult instanceof type.errors) {
109
+ console.error(userResult.summary);
110
+ } else {
111
+ // userResult is validated user data
112
+ console.log(userResult);
113
+ }
114
+
115
+ // Validate create input
116
+ const createData = {
117
+ email: "user@example.com",
118
+ name: "John Doe"
119
+ };
120
+
121
+ const createResult = UserCreate(createData);
122
+ // ...
123
+
124
+ // Validate where clauses
125
+ const whereClause = {
126
+ email: "user@example.com"
127
+ };
128
+
129
+ const whereResult = UserWhere(whereClause);
130
+ // ...
131
+ ```
132
+
133
+ ## Annotations
134
+
135
+ Control schema generation using annotations in your Prisma schema:
136
+
137
+ ### Hide Fields/Models
138
+
139
+ ```prisma
140
+ /// @prisma-arktype.hide
141
+ model InternalModel {
142
+ id String @id
143
+ }
144
+
145
+ model User {
146
+ id String @id
147
+ /// @prisma-arktype.hide
148
+ internalField String
149
+ }
150
+ ```
151
+
152
+ ### Hide from Input Models
153
+
154
+ ```prisma
155
+ model User {
156
+ id String @id
157
+ /// @prisma-arktype.input.hide
158
+ computedField String
159
+ /// @prisma-arktype.create.input.hide
160
+ onlyInUpdates String
161
+ /// @prisma-arktype.update.input.hide
162
+ onlyInCreates String
163
+ }
164
+ ```
165
+
166
+ ### Type Override
167
+
168
+ ```prisma
169
+ model User {
170
+ id String @id
171
+ /// @prisma-arktype.typeOverwrite="string.email"
172
+ email String
173
+ /// @prisma-arktype.typeOverwrite="string.numeric"
174
+ phone String
175
+ }
176
+ ```
177
+
178
+ ### Custom Options
179
+
180
+ ```prisma
181
+ model User {
182
+ id String @id
183
+ /// @prisma-arktype.options{minLength: 3, maxLength: 50}
184
+ username String
185
+ }
186
+ ```
187
+
188
+ ## Type Mapping
189
+
190
+ Prisma types are mapped to ArkType as follows:
191
+
192
+ | Prisma Type | ArkType Type |
193
+ |-------------|--------------|
194
+ | `String` | `"string"` |
195
+ | `Int` | `"integer"` |
196
+ | `BigInt` | `"integer"` |
197
+ | `Float` | `"number"` |
198
+ | `Decimal` | `"number"` |
199
+ | `Boolean` | `"boolean"` |
200
+ | `DateTime` | `"Date"` |
201
+ | `Json` | `"unknown"` |
202
+ | `Bytes` | `"instanceof Buffer"` |
203
+ | Enums | Union of literal values |
204
+
205
+ ## Differences from prismabox
206
+
207
+ While this package is inspired by prismabox, there are some key differences:
208
+
209
+ 1. **ArkType vs TypeBox**: Uses ArkType's syntax and type system instead of TypeBox
210
+ 2. **Simpler type definitions**: ArkType's string-based syntax makes schemas more readable
211
+ 3. **No nullable wrapper**: ArkType handles nullable types directly with union syntax
212
+ 4. **Different validation API**: Uses ArkType's validation approach
213
+
214
+ ## Development
215
+
216
+ ### Setup
217
+
218
+ ```bash
219
+ # Clone the repository
220
+ git clone https://github.com/yourusername/prisma-arktype.git
221
+ cd prisma-arktype
222
+
223
+ # Install dependencies
224
+ pnpm install
225
+
226
+ # Build the project
227
+ pnpm build
228
+
229
+ # Run tests
230
+ pnpm test
231
+
232
+ # Run tests in watch mode
233
+ pnpm test:watch
234
+
235
+ # Run linter
236
+ pnpm lint
237
+
238
+ # Fix linting issues
239
+ pnpm lint:fix
240
+ ```
241
+
242
+ ### Publishing
243
+
244
+ This project uses [Changesets](https://github.com/changesets/changesets) for version management and publishing.
245
+
246
+ #### Creating a changeset
247
+
248
+ When you make changes that should be included in the next release:
249
+
250
+ ```bash
251
+ pnpm changeset
252
+ ```
253
+
254
+ This will prompt you to:
255
+ 1. Select the type of change (major, minor, patch)
256
+ 2. Provide a description of the changes
257
+
258
+ Commit the generated changeset file along with your changes.
259
+
260
+ #### Publishing workflow
261
+
262
+ 1. **Create a changeset** for your changes
263
+ 2. **Open a PR** with your changes and the changeset
264
+ 3. **Merge the PR** - The GitHub Action will automatically create a "Version Packages" PR
265
+ 4. **Review and merge** the Version Packages PR - This will:
266
+ - Update the version in package.json
267
+ - Update the CHANGELOG.md
268
+ - Publish the package to npm
269
+ - Create a GitHub release
270
+
271
+ #### Manual publishing (maintainers only)
272
+
273
+ ```bash
274
+ # Build and publish
275
+ pnpm release
276
+ ```
277
+
278
+ **Prerequisites:**
279
+ - Set up `NPM_TOKEN` secret in GitHub repository settings
280
+ - Ensure you have publish access to the npm package
281
+
282
+ ## Contributing
283
+
284
+ Contributions are welcome! Please feel free to submit a Pull Request.
285
+
286
+ 1. Fork the repository
287
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
288
+ 3. Make your changes
289
+ 4. Create a changeset (`pnpm changeset`)
290
+ 5. Commit your changes following the commit message format (see below)
291
+ 6. Push to the branch (`git push origin feature/amazing-feature`)
292
+ 7. Open a Pull Request
293
+
294
+ ### Commit Message Format
295
+
296
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/). Commit messages are automatically linted using commitlint and lefthook.
297
+
298
+ Format: `<type>(<scope>): <subject>`
299
+
300
+ **Types:**
301
+ - `feat`: New feature
302
+ - `fix`: Bug fix
303
+ - `docs`: Documentation changes
304
+ - `style`: Code style changes (formatting, etc.)
305
+ - `refactor`: Code refactoring
306
+ - `perf`: Performance improvements
307
+ - `test`: Adding or updating tests
308
+ - `build`: Build system changes
309
+ - `ci`: CI/CD changes
310
+ - `chore`: Other changes
311
+
312
+ **Examples:**
313
+ ```bash
314
+ git commit -m "feat: add support for custom type validators"
315
+ git commit -m "fix: resolve issue with nullable DateTime fields"
316
+ git commit -m "docs: update installation instructions"
317
+ git commit -m "refactor: simplify where clause generation"
318
+ ```
319
+
320
+ ### Git Hooks
321
+
322
+ This project uses [lefthook](https://github.com/evilmartians/lefthook) to manage git hooks:
323
+
324
+ - **commit-msg**: Validates commit message format
325
+ - **pre-commit**: Runs linter and checks for debug statements
326
+ - **pre-push**: Runs tests before pushing
327
+
328
+ To skip hooks (use sparingly):
329
+ ```bash
330
+ git commit --no-verify -m "your message"
331
+ ```
332
+
333
+ ## License
334
+
335
+ MIT
336
+
337
+ ## Credits
338
+
339
+ This package is heavily based on [prismabox](https://github.com/m1212e/prismabox) by m1212e. Many thanks for the excellent foundation and architecture!
340
+
341
+ ## Related Projects
342
+
343
+ - [ArkType](https://arktype.io) - TypeScript's 1:1 validator
344
+ - [Prisma](https://www.prisma.io) - Next-generation ORM for Node.js & TypeScript
345
+ - [prismabox](https://github.com/m1212e/prismabox) - Generate TypeBox schemas from Prisma (inspiration for this project)
@@ -0,0 +1,2 @@
1
+
2
+ export { };
package/dist/index.js ADDED
@@ -0,0 +1,829 @@
1
+ #!/usr/bin/env node
2
+ import { writeFile, access, rm, mkdir } from 'node:fs/promises';
3
+ import generatorHelperPkg from '@prisma/generator-helper';
4
+ import { type, ArkErrors } from 'arktype';
5
+ import { join } from 'node:path';
6
+
7
+ const configSchema = type({
8
+ output: "string = './prisma/generated/validators'",
9
+ arktypeImportDependencyName: "string = 'arktype'",
10
+ ignoredKeysOnInputModels: "string[]"
11
+ });
12
+ let config = {
13
+ output: "./prisma/generated/validators",
14
+ arktypeImportDependencyName: "arktype",
15
+ ignoredKeysOnInputModels: ["id", "createdAt", "updatedAt"]
16
+ };
17
+ const configPartialSchema = configSchema.partial();
18
+ function setConfig(newConfig) {
19
+ const out = configPartialSchema(newConfig);
20
+ if (out instanceof ArkErrors) {
21
+ throw new Error(`Invalid generator config:
22
+ ${out.toString()}`);
23
+ }
24
+ config = { ...config, ...out };
25
+ Object.freeze(config);
26
+ }
27
+ function getConfig() {
28
+ return config;
29
+ }
30
+
31
+ function isHidden(annotation) {
32
+ return annotation.type === "HIDDEN";
33
+ }
34
+ function isHiddenInput(annotation) {
35
+ return annotation.type === "HIDDEN_INPUT";
36
+ }
37
+ function isHiddenInputCreate(annotation) {
38
+ return annotation.type === "HIDDEN_INPUT_CREATE";
39
+ }
40
+ function isHiddenInputUpdate(annotation) {
41
+ return annotation.type === "HIDDEN_INPUT_UPDATE";
42
+ }
43
+ function isOptions(annotation) {
44
+ return annotation.type === "OPTIONS";
45
+ }
46
+ function isTypeOverwrite(annotation) {
47
+ return annotation.type === "TYPE_OVERWRITE";
48
+ }
49
+ const annotationKeys = [
50
+ {
51
+ keys: ["@prisma-arktype.hide", "@prisma-arktype.hidden"],
52
+ type: "HIDDEN"
53
+ },
54
+ {
55
+ keys: ["@prisma-arktype.input.hide", "@prisma-arktype.input.hidden"],
56
+ type: "HIDDEN_INPUT"
57
+ },
58
+ {
59
+ keys: [
60
+ "@prisma-arktype.create.input.hide",
61
+ "@prisma-arktype.create.input.hidden"
62
+ ],
63
+ type: "HIDDEN_INPUT_CREATE"
64
+ },
65
+ {
66
+ keys: [
67
+ "@prisma-arktype.update.input.hide",
68
+ "@prisma-arktype.update.input.hidden"
69
+ ],
70
+ type: "HIDDEN_INPUT_UPDATE"
71
+ },
72
+ { keys: ["@prisma-arktype.options"], type: "OPTIONS" },
73
+ { keys: ["@prisma-arktype.typeOverwrite"], type: "TYPE_OVERWRITE" }
74
+ ];
75
+ const prismaArktypeOptionsRegex = /@prisma-arktype\.options\{(.+)\}/;
76
+ const prismaArktypeTypeOverwriteRegex = /@prisma-arktype\.typeOverwrite=(.+)/;
77
+ function extractAnnotations(documentation) {
78
+ const annotations = [];
79
+ const descriptionLines = [];
80
+ if (documentation) {
81
+ for (const line of documentation.split("\n")) {
82
+ let isAnnotation = false;
83
+ for (const { keys, type } of annotationKeys) {
84
+ for (const key of keys) {
85
+ if (line.includes(key)) {
86
+ isAnnotation = true;
87
+ if (type === "OPTIONS") {
88
+ const match = line.match(prismaArktypeOptionsRegex);
89
+ if (match && match[1]) {
90
+ annotations.push({ type: "OPTIONS", value: match[1] });
91
+ } else {
92
+ throw new Error(`Invalid OPTIONS annotation: ${line}`);
93
+ }
94
+ } else if (type === "TYPE_OVERWRITE") {
95
+ const match = line.match(prismaArktypeTypeOverwriteRegex);
96
+ if (match && match[1]) {
97
+ annotations.push({
98
+ type: "TYPE_OVERWRITE",
99
+ value: match[1].trim()
100
+ });
101
+ } else {
102
+ throw new Error(`Invalid TYPE_OVERWRITE annotation: ${line}`);
103
+ }
104
+ } else {
105
+ annotations.push({ type });
106
+ }
107
+ break;
108
+ }
109
+ }
110
+ if (isAnnotation) break;
111
+ }
112
+ if (!isAnnotation) {
113
+ descriptionLines.push(line);
114
+ }
115
+ }
116
+ }
117
+ return {
118
+ annotations,
119
+ description: descriptionLines.join("\n").trim(),
120
+ hidden: annotations.some(isHidden),
121
+ hiddenInput: annotations.some(isHiddenInput),
122
+ hiddenInputCreate: annotations.some(isHiddenInputCreate),
123
+ hiddenInputUpdate: annotations.some(isHiddenInputUpdate)
124
+ };
125
+ }
126
+ function generateArktypeOptions(annotations) {
127
+ const optionsAnnotations = annotations.filter(isOptions);
128
+ if (optionsAnnotations.length === 0) {
129
+ return "";
130
+ }
131
+ return `.pipe(${optionsAnnotations.map((a) => a.value).join(", ")})`;
132
+ }
133
+
134
+ const primitiveTypes = [
135
+ "Int",
136
+ "BigInt",
137
+ "Float",
138
+ "Decimal",
139
+ "String",
140
+ "DateTime",
141
+ "Json",
142
+ "Boolean",
143
+ "Bytes"
144
+ ];
145
+ function isPrimitivePrismaFieldType(type) {
146
+ return primitiveTypes.includes(type);
147
+ }
148
+ function stringifyPrimitiveType(type, annotations) {
149
+ const options = generateArktypeOptions(annotations);
150
+ switch (type) {
151
+ case "Int":
152
+ case "BigInt":
153
+ return `"number.integer"${options}`;
154
+ case "Float":
155
+ case "Decimal":
156
+ return `"number"${options}`;
157
+ case "String":
158
+ return `"string"${options}`;
159
+ case "DateTime":
160
+ return `"Date"${options}`;
161
+ case "Json":
162
+ return `"unknown"${options}`;
163
+ case "Boolean":
164
+ return `"boolean"${options}`;
165
+ case "Bytes":
166
+ return `"instanceof Buffer"${options}`;
167
+ default:
168
+ throw new Error(`Unsupported primitive type: ${type}`);
169
+ }
170
+ }
171
+
172
+ function wrapWithArray(input) {
173
+ return `${input}[]`;
174
+ }
175
+ function makeUnion(inputModels) {
176
+ return inputModels.join(" | ");
177
+ }
178
+
179
+ const processedEnums = [];
180
+ function processEnums(enums) {
181
+ for (const enumData of enums) {
182
+ const stringified = stringifyEnum(enumData);
183
+ if (stringified) {
184
+ processedEnums.push({
185
+ name: enumData.name,
186
+ stringified
187
+ });
188
+ }
189
+ }
190
+ Object.freeze(processedEnums);
191
+ }
192
+ function stringifyEnum(enumData) {
193
+ const { annotations, hidden } = extractAnnotations(enumData.documentation);
194
+ if (hidden) {
195
+ return;
196
+ }
197
+ const values = enumData.values.map((v) => `'${v.name}'`);
198
+ const options = generateArktypeOptions(annotations);
199
+ return `type("${makeUnion(values)}")${options}`;
200
+ }
201
+
202
+ const processedPlain = [];
203
+ function processPlain(models) {
204
+ for (const model of models) {
205
+ const stringified = stringifyPlain(model);
206
+ if (stringified) {
207
+ processedPlain.push({
208
+ name: model.name,
209
+ stringified
210
+ });
211
+ }
212
+ }
213
+ Object.freeze(processedPlain);
214
+ }
215
+ const enumMatch$1 = /type\("(.+)"\)/;
216
+ function stringifyPlain(model, isInputCreate = false, isInputUpdate = false) {
217
+ const config = getConfig();
218
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
219
+ model.documentation
220
+ );
221
+ if (hidden) {
222
+ return;
223
+ }
224
+ const fields = [];
225
+ for (const field of model.fields) {
226
+ const {
227
+ annotations: fieldAnnotations,
228
+ hidden: fieldHidden,
229
+ hiddenInput,
230
+ hiddenInputCreate,
231
+ hiddenInputUpdate
232
+ } = extractAnnotations(field.documentation);
233
+ if (fieldHidden) continue;
234
+ if (isInputCreate && (hiddenInput || hiddenInputCreate)) continue;
235
+ if (isInputUpdate && (hiddenInput || hiddenInputUpdate)) continue;
236
+ if (field.kind === "object") continue;
237
+ if (isInputCreate || isInputUpdate) {
238
+ if (config.ignoredKeysOnInputModels.includes(field.name)) continue;
239
+ if (field.name.endsWith("Id") && field.relationName) continue;
240
+ }
241
+ const typeOverwrite = fieldAnnotations.find(isTypeOverwrite);
242
+ let fieldType;
243
+ let fieldName = field.name;
244
+ if (typeOverwrite) {
245
+ fieldType = typeOverwrite.value;
246
+ } else if (isPrimitivePrismaFieldType(field.type)) {
247
+ fieldType = stringifyPrimitiveType(field.type, fieldAnnotations);
248
+ } else if (field.kind === "enum") {
249
+ const enumDef = processedEnums.find((e) => e.name === field.type);
250
+ if (!enumDef) continue;
251
+ const match = enumDef.stringified.match(enumMatch$1);
252
+ fieldType = match ? `"${match[1]}"` : `"'${field.type}'"`;
253
+ } else {
254
+ continue;
255
+ }
256
+ if (field.isList) {
257
+ fieldType = `"${wrapWithArray(fieldType.slice(1, -1))}"`;
258
+ }
259
+ if (!field.isRequired) {
260
+ const inner = fieldType.slice(1, -1);
261
+ fieldType = `"${inner} | null"`;
262
+ fieldName += "?";
263
+ }
264
+ if (field.hasDefaultValue || isInputUpdate) {
265
+ if (!fieldName.endsWith("?")) {
266
+ fieldName += "?";
267
+ }
268
+ }
269
+ fields.push(`"${fieldName}": ${fieldType}`);
270
+ }
271
+ const options = generateArktypeOptions(modelAnnotations);
272
+ return `{
273
+ ${fields.join(",\n ")}
274
+ }${options}`;
275
+ }
276
+ function stringifyPlainInputCreate(model) {
277
+ return stringifyPlain(model, true, false);
278
+ }
279
+ function stringifyPlainInputUpdate(model) {
280
+ return stringifyPlain(model, false, true);
281
+ }
282
+
283
+ const processedCreate = [];
284
+ function processCreate(models) {
285
+ for (const model of models) {
286
+ const { hidden } = extractAnnotations(model.documentation);
287
+ if (hidden) continue;
288
+ const stringified = stringifyPlainInputCreate(model);
289
+ if (stringified) {
290
+ processedCreate.push({
291
+ name: model.name,
292
+ stringified
293
+ });
294
+ }
295
+ }
296
+ Object.freeze(processedCreate);
297
+ }
298
+
299
+ const processedInclude = [];
300
+ function processInclude(models) {
301
+ for (const model of models) {
302
+ const stringified = stringifyInclude(model);
303
+ if (stringified) {
304
+ processedInclude.push({
305
+ name: model.name,
306
+ stringified
307
+ });
308
+ }
309
+ }
310
+ Object.freeze(processedInclude);
311
+ }
312
+ function stringifyInclude(model) {
313
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
314
+ model.documentation
315
+ );
316
+ if (hidden) {
317
+ return;
318
+ }
319
+ const fields = [];
320
+ for (const field of model.fields) {
321
+ const { hidden: fieldHidden } = extractAnnotations(field.documentation);
322
+ if (fieldHidden) continue;
323
+ if (field.kind !== "object") continue;
324
+ fields.push(`"${field.name}?": "boolean"`);
325
+ }
326
+ fields.push(`"_count?": "boolean"`);
327
+ if (fields.length <= 1) {
328
+ return;
329
+ }
330
+ const options = generateArktypeOptions(modelAnnotations);
331
+ return `{
332
+ ${fields.join(",\n ")}
333
+ }${options}`;
334
+ }
335
+
336
+ const processedOrderBy = [];
337
+ function processOrderBy(models) {
338
+ for (const model of models) {
339
+ const stringified = stringifyOrderBy(model);
340
+ if (stringified) {
341
+ processedOrderBy.push({
342
+ name: model.name,
343
+ stringified
344
+ });
345
+ }
346
+ }
347
+ Object.freeze(processedOrderBy);
348
+ }
349
+ function stringifyOrderBy(model) {
350
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
351
+ model.documentation
352
+ );
353
+ if (hidden) {
354
+ return;
355
+ }
356
+ const fields = [];
357
+ const sortOrder = `"'asc' | 'desc'"`;
358
+ for (const field of model.fields) {
359
+ const { hidden: fieldHidden } = extractAnnotations(field.documentation);
360
+ if (fieldHidden) continue;
361
+ if (field.kind === "object") continue;
362
+ fields.push(`"${field.name}?": ${sortOrder}`);
363
+ }
364
+ const options = generateArktypeOptions(modelAnnotations);
365
+ return `{
366
+ ${fields.join(",\n ")}
367
+ }${options}`;
368
+ }
369
+
370
+ const processedRelations = [];
371
+ const processedRelationsCreate = [];
372
+ const processedRelationsUpdate = [];
373
+ function processRelations(models) {
374
+ for (const model of models) {
375
+ const stringified = stringifyRelations(model);
376
+ if (stringified) {
377
+ processedRelations.push({
378
+ name: model.name,
379
+ stringified
380
+ });
381
+ }
382
+ }
383
+ Object.freeze(processedRelations);
384
+ }
385
+ function stringifyRelations(model) {
386
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
387
+ model.documentation
388
+ );
389
+ if (hidden) {
390
+ return;
391
+ }
392
+ const fields = [];
393
+ for (const field of model.fields) {
394
+ const { hidden: fieldHidden } = extractAnnotations(field.documentation);
395
+ if (fieldHidden) continue;
396
+ if (field.kind !== "object") continue;
397
+ let fieldType = `"unknown"`;
398
+ if (field.isList) {
399
+ fieldType = `"unknown[]"`;
400
+ }
401
+ if (!field.isRequired) {
402
+ fieldType = `"unknown | null"`;
403
+ }
404
+ fields.push(`"${field.name}": ${fieldType}`);
405
+ }
406
+ if (fields.length === 0) {
407
+ return;
408
+ }
409
+ const options = generateArktypeOptions(modelAnnotations);
410
+ return `{
411
+ ${fields.join(",\n ")}
412
+ }${options}`;
413
+ }
414
+ function processRelationsCreate(models) {
415
+ for (const model of models) {
416
+ const stringified = stringifyRelationsInputCreate(model, models);
417
+ if (stringified) {
418
+ processedRelationsCreate.push({
419
+ name: model.name,
420
+ stringified
421
+ });
422
+ }
423
+ }
424
+ Object.freeze(processedRelationsCreate);
425
+ }
426
+ function stringifyRelationsInputCreate(model, allModels) {
427
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
428
+ model.documentation
429
+ );
430
+ if (hidden) {
431
+ return;
432
+ }
433
+ const fields = [];
434
+ for (const field of model.fields) {
435
+ const {
436
+ hidden: fieldHidden,
437
+ hiddenInput,
438
+ hiddenInputCreate
439
+ } = extractAnnotations(field.documentation);
440
+ if (fieldHidden || hiddenInput || hiddenInputCreate) continue;
441
+ if (field.kind !== "object") continue;
442
+ const relatedModel = allModels.find((m) => m.name === field.type);
443
+ if (!relatedModel) continue;
444
+ const idField = relatedModel.fields.find((f) => f.isId);
445
+ let idType = '"string"';
446
+ if (idField) {
447
+ if (idField.type === "Int" || idField.type === "BigInt") {
448
+ idType = '"integer"';
449
+ }
450
+ }
451
+ const connectType = `{ "connect": { "id": ${idType} } }`;
452
+ fields.push(`"${field.name}?": ${connectType}`);
453
+ }
454
+ if (fields.length === 0) {
455
+ return;
456
+ }
457
+ const options = generateArktypeOptions(modelAnnotations);
458
+ return `{
459
+ ${fields.join(",\n ")}
460
+ }${options}`;
461
+ }
462
+ function processRelationsUpdate(models) {
463
+ for (const model of models) {
464
+ const stringified = stringifyRelationsInputUpdate(model, models);
465
+ if (stringified) {
466
+ processedRelationsUpdate.push({
467
+ name: model.name,
468
+ stringified
469
+ });
470
+ }
471
+ }
472
+ Object.freeze(processedRelationsUpdate);
473
+ }
474
+ function stringifyRelationsInputUpdate(model, allModels) {
475
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
476
+ model.documentation
477
+ );
478
+ if (hidden) {
479
+ return;
480
+ }
481
+ const fields = [];
482
+ for (const field of model.fields) {
483
+ const {
484
+ hidden: fieldHidden,
485
+ hiddenInput,
486
+ hiddenInputUpdate
487
+ } = extractAnnotations(field.documentation);
488
+ if (fieldHidden || hiddenInput || hiddenInputUpdate) continue;
489
+ if (field.kind !== "object") continue;
490
+ const relatedModel = allModels.find((m) => m.name === field.type);
491
+ if (!relatedModel) continue;
492
+ const idField = relatedModel.fields.find((f) => f.isId);
493
+ let idType = '"string"';
494
+ if (idField) {
495
+ if (idField.type === "Int" || idField.type === "BigInt") {
496
+ idType = '"integer"';
497
+ }
498
+ }
499
+ let fieldType;
500
+ if (field.isList) {
501
+ fieldType = `{ "connect"?: { "id": ${idType} }[], "disconnect"?: { "id": ${idType} }[] }`;
502
+ } else {
503
+ if (field.isRequired) {
504
+ fieldType = `{ "connect": { "id": ${idType} } }`;
505
+ } else {
506
+ fieldType = `{ "connect"?: { "id": ${idType} }, "disconnect"?: "boolean" }`;
507
+ }
508
+ }
509
+ fields.push(`"${field.name}?": ${fieldType}`);
510
+ }
511
+ if (fields.length === 0) {
512
+ return;
513
+ }
514
+ const options = generateArktypeOptions(modelAnnotations);
515
+ return `{
516
+ ${fields.join(",\n ")}
517
+ }${options}`;
518
+ }
519
+
520
+ const processedSelect = [];
521
+ function processSelect(models) {
522
+ for (const model of models) {
523
+ const stringified = stringifySelect(model);
524
+ if (stringified) {
525
+ processedSelect.push({
526
+ name: model.name,
527
+ stringified
528
+ });
529
+ }
530
+ }
531
+ Object.freeze(processedSelect);
532
+ }
533
+ function stringifySelect(model) {
534
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
535
+ model.documentation
536
+ );
537
+ if (hidden) {
538
+ return;
539
+ }
540
+ const fields = [];
541
+ for (const field of model.fields) {
542
+ const { hidden: fieldHidden } = extractAnnotations(field.documentation);
543
+ if (fieldHidden) continue;
544
+ fields.push(`"${field.name}?": "boolean"`);
545
+ }
546
+ fields.push(`"_count?": "boolean"`);
547
+ const options = generateArktypeOptions(modelAnnotations);
548
+ return `{
549
+ ${fields.join(",\n ")}
550
+ }${options}`;
551
+ }
552
+
553
+ const processedUpdate = [];
554
+ function processUpdate(models) {
555
+ for (const model of models) {
556
+ const { hidden } = extractAnnotations(model.documentation);
557
+ if (hidden) continue;
558
+ const stringified = stringifyPlainInputUpdate(model);
559
+ if (stringified) {
560
+ processedUpdate.push({
561
+ name: model.name,
562
+ stringified
563
+ });
564
+ }
565
+ }
566
+ Object.freeze(processedUpdate);
567
+ }
568
+
569
+ const processedWhere = [];
570
+ const processedWhereUnique = [];
571
+ function processWhere(models) {
572
+ for (const model of models) {
573
+ const stringified = stringifyWhere(model);
574
+ if (stringified) {
575
+ processedWhere.push({
576
+ name: model.name,
577
+ stringified
578
+ });
579
+ }
580
+ const stringifiedUnique = stringifyWhereUnique(model);
581
+ if (stringifiedUnique) {
582
+ processedWhereUnique.push({
583
+ name: model.name,
584
+ stringified: stringifiedUnique
585
+ });
586
+ }
587
+ }
588
+ Object.freeze(processedWhere);
589
+ Object.freeze(processedWhereUnique);
590
+ }
591
+ const enumMatch = /type\("(.+)"\)/;
592
+ function stringifyWhere(model) {
593
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
594
+ model.documentation
595
+ );
596
+ if (hidden) {
597
+ return;
598
+ }
599
+ const fields = [];
600
+ for (const field of model.fields) {
601
+ const { annotations: fieldAnnotations, hidden: fieldHidden } = extractAnnotations(field.documentation);
602
+ if (fieldHidden) continue;
603
+ if (field.kind === "object") continue;
604
+ const typeOverwrite = fieldAnnotations.find(isTypeOverwrite);
605
+ let fieldType;
606
+ if (typeOverwrite) {
607
+ fieldType = `"${typeOverwrite.value}"`;
608
+ } else if (isPrimitivePrismaFieldType(field.type)) {
609
+ fieldType = stringifyPrimitiveType(field.type, fieldAnnotations);
610
+ } else if (field.kind === "enum") {
611
+ const enumDef = processedEnums.find((e) => e.name === field.type);
612
+ if (!enumDef) continue;
613
+ const match = enumDef.stringified.match(enumMatch);
614
+ fieldType = match ? `"${match[1]}"` : `"'${field.type}'"`;
615
+ } else {
616
+ continue;
617
+ }
618
+ if (field.isList) {
619
+ const inner = fieldType.slice(1, -1);
620
+ fieldType = `"${wrapWithArray(inner)}"`;
621
+ }
622
+ fields.push(`"${field.name}?": ${fieldType}`);
623
+ }
624
+ const options = generateArktypeOptions(modelAnnotations);
625
+ return `{
626
+ ${fields.join(",\n ")}
627
+ }${options}`;
628
+ }
629
+ function stringifyWhereUnique(model) {
630
+ const { annotations: modelAnnotations, hidden } = extractAnnotations(
631
+ model.documentation
632
+ );
633
+ if (hidden) {
634
+ return;
635
+ }
636
+ const fields = [];
637
+ for (const field of model.fields) {
638
+ const { annotations: fieldAnnotations, hidden: fieldHidden } = extractAnnotations(field.documentation);
639
+ if (fieldHidden) continue;
640
+ if (field.kind === "object") continue;
641
+ if (!(field.isId || field.isUnique)) continue;
642
+ const typeOverwrite = fieldAnnotations.find(isTypeOverwrite);
643
+ let fieldType;
644
+ if (typeOverwrite) {
645
+ fieldType = `"${typeOverwrite.value}"`;
646
+ } else if (isPrimitivePrismaFieldType(field.type)) {
647
+ fieldType = stringifyPrimitiveType(field.type, fieldAnnotations);
648
+ } else if (field.kind === "enum") {
649
+ const enumDef = processedEnums.find((e) => e.name === field.type);
650
+ if (!enumDef) continue;
651
+ const match = enumDef.stringified.match(enumMatch);
652
+ fieldType = match ? `"${match[1]}"` : `"'${field.type}'"`;
653
+ } else {
654
+ continue;
655
+ }
656
+ fields.push(`"${field.name}?": ${fieldType}`);
657
+ }
658
+ if (fields.length === 0) {
659
+ return;
660
+ }
661
+ const options = generateArktypeOptions(modelAnnotations);
662
+ return `{
663
+ ${fields.join(",\n ")}
664
+ }${options}`;
665
+ }
666
+
667
+ async function format(input) {
668
+ return input;
669
+ }
670
+ function mapAllModelsForWrite(processedEnums, processedPlain, processedRelations, processedWhere, processedWhereUnique, processedCreate, processedUpdate, processedRelationsCreate, processedRelationsUpdate, processedSelect, processedInclude, processedOrderBy) {
671
+ const config = getConfig();
672
+ const modelMap = /* @__PURE__ */ new Map();
673
+ const arktypeImport = `import { type } from "${config.arktypeImportDependencyName}";
674
+
675
+ `;
676
+ for (const model of processedEnums) {
677
+ const content = `${arktypeImport}export const ${model.name} = ${model.stringified};
678
+ `;
679
+ modelMap.set(model.name, content);
680
+ }
681
+ for (const model of processedPlain) {
682
+ const content = `${arktypeImport}export const ${model.name}Plain = type(${model.stringified});
683
+ `;
684
+ modelMap.set(`${model.name}Plain`, content);
685
+ }
686
+ for (const model of processedRelations) {
687
+ const content = `${arktypeImport}export const ${model.name}Relations = type(${model.stringified});
688
+ `;
689
+ modelMap.set(`${model.name}Relations`, content);
690
+ }
691
+ for (const plain of processedPlain) {
692
+ const hasRelations = processedRelations.some((r) => r.name === plain.name);
693
+ if (hasRelations) {
694
+ const content = `${arktypeImport}import { ${plain.name}Plain } from "./${plain.name}Plain";
695
+ import { ${plain.name}Relations } from "./${plain.name}Relations";
696
+
697
+ export const ${plain.name} = type(() => ${plain.name}Plain.and(${plain.name}Relations));
698
+ `;
699
+ modelMap.set(plain.name, content);
700
+ } else {
701
+ const content = `${arktypeImport}import { ${plain.name}Plain } from "./${plain.name}Plain";
702
+
703
+ export const ${plain.name} = ${plain.name}Plain;
704
+ `;
705
+ modelMap.set(plain.name, content);
706
+ }
707
+ }
708
+ for (const model of processedWhere) {
709
+ const content = `${arktypeImport}export const ${model.name}Where = type(${model.stringified});
710
+ `;
711
+ modelMap.set(`${model.name}Where`, content);
712
+ }
713
+ for (const model of processedWhereUnique) {
714
+ const content = `${arktypeImport}export const ${model.name}WhereUnique = type(${model.stringified});
715
+ `;
716
+ modelMap.set(`${model.name}WhereUnique`, content);
717
+ }
718
+ for (const model of processedCreate) {
719
+ const content = `${arktypeImport}export const ${model.name}Create = type(${model.stringified});
720
+ `;
721
+ modelMap.set(`${model.name}Create`, content);
722
+ }
723
+ for (const model of processedUpdate) {
724
+ const content = `${arktypeImport}export const ${model.name}Update = type(${model.stringified});
725
+ `;
726
+ modelMap.set(`${model.name}Update`, content);
727
+ }
728
+ for (const model of processedRelationsCreate) {
729
+ const content = `${arktypeImport}export const ${model.name}RelationsCreate = type(${model.stringified});
730
+ `;
731
+ modelMap.set(`${model.name}RelationsCreate`, content);
732
+ }
733
+ for (const model of processedRelationsUpdate) {
734
+ const content = `${arktypeImport}export const ${model.name}RelationsUpdate = type(${model.stringified});
735
+ `;
736
+ modelMap.set(`${model.name}RelationsUpdate`, content);
737
+ }
738
+ for (const model of processedSelect) {
739
+ const content = `${arktypeImport}export const ${model.name}Select = type(${model.stringified});
740
+ `;
741
+ modelMap.set(`${model.name}Select`, content);
742
+ }
743
+ for (const model of processedInclude) {
744
+ const content = `${arktypeImport}export const ${model.name}Include = type(${model.stringified});
745
+ `;
746
+ modelMap.set(`${model.name}Include`, content);
747
+ }
748
+ for (const model of processedOrderBy) {
749
+ const content = `${arktypeImport}export const ${model.name}OrderBy = type(${model.stringified});
750
+ `;
751
+ modelMap.set(`${model.name}OrderBy`, content);
752
+ }
753
+ return modelMap;
754
+ }
755
+ async function write(processedEnums, processedPlain, processedRelations, processedWhere, processedWhereUnique, processedCreate, processedUpdate, processedRelationsCreate, processedRelationsUpdate, processedSelect, processedInclude, processedOrderBy) {
756
+ const config = getConfig();
757
+ const modelMap = mapAllModelsForWrite(
758
+ processedEnums,
759
+ processedPlain,
760
+ processedRelations,
761
+ processedWhere,
762
+ processedWhereUnique,
763
+ processedCreate,
764
+ processedUpdate,
765
+ processedRelationsCreate,
766
+ processedRelationsUpdate,
767
+ processedSelect,
768
+ processedInclude,
769
+ processedOrderBy
770
+ );
771
+ const writePromises = [];
772
+ for (const [name, content] of modelMap.entries()) {
773
+ const filePath = join(config.output, `${name}.ts`);
774
+ writePromises.push(writeFile(filePath, await format(content)));
775
+ }
776
+ const barrelExports = Array.from(modelMap.keys()).map((name) => `export * from "./${name}";`).join("\n");
777
+ writePromises.push(
778
+ writeFile(join(config.output, "index.ts"), await format(barrelExports))
779
+ );
780
+ await Promise.all(writePromises);
781
+ }
782
+
783
+ const { generatorHandler } = generatorHelperPkg;
784
+ generatorHandler({
785
+ onManifest() {
786
+ return {
787
+ defaultOutput: "./prisma/generated/validators",
788
+ prettyName: "prisma-arktype"
789
+ };
790
+ },
791
+ async onGenerate(options) {
792
+ setConfig({
793
+ ...options.generator.config,
794
+ output: options.generator.output?.value
795
+ });
796
+ try {
797
+ await access(getConfig().output);
798
+ await rm(getConfig().output, { recursive: true });
799
+ } catch {
800
+ }
801
+ await mkdir(getConfig().output, { recursive: true });
802
+ processEnums(options.dmmf.datamodel.enums);
803
+ processPlain(options.dmmf.datamodel.models);
804
+ processWhere(options.dmmf.datamodel.models);
805
+ processCreate(options.dmmf.datamodel.models);
806
+ processUpdate(options.dmmf.datamodel.models);
807
+ processSelect(options.dmmf.datamodel.models);
808
+ processInclude(options.dmmf.datamodel.models);
809
+ processOrderBy(options.dmmf.datamodel.models);
810
+ processRelations(options.dmmf.datamodel.models);
811
+ processRelationsCreate(options.dmmf.datamodel.models);
812
+ processRelationsUpdate(options.dmmf.datamodel.models);
813
+ await write(
814
+ processedEnums,
815
+ processedPlain,
816
+ processedRelations,
817
+ processedWhere,
818
+ processedWhereUnique,
819
+ processedCreate,
820
+ processedUpdate,
821
+ processedRelationsCreate,
822
+ processedRelationsUpdate,
823
+ processedSelect,
824
+ processedInclude,
825
+ processedOrderBy
826
+ );
827
+ console.info("\u2705 prisma-arktype: Generated ArkType schemas successfully!");
828
+ }
829
+ });
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "prisma-arktype",
3
+ "version": "2.0.0",
4
+ "description": "Generate ArkType schemas from your Prisma schema",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "bin": {
15
+ "prisma-arktype": "./dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "keywords": [
21
+ "prisma",
22
+ "arktype",
23
+ "validation",
24
+ "schema",
25
+ "generator",
26
+ "typesafe"
27
+ ],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/GeorgeIpsum/prisma-arktype.git"
33
+ },
34
+ "homepage": "https://github.com/GeorgeIpsum/prisma-arktype#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/GeorgeIpsum/prisma-arktype/issues"
37
+ },
38
+ "dependencies": {
39
+ "@prisma/generator-helper": "^6.18.0",
40
+ "arktype": "^2.1.25"
41
+ },
42
+ "devDependencies": {
43
+ "@biomejs/biome": "^2.3.3",
44
+ "@changesets/cli": "^2.29.7",
45
+ "@commitlint/cli": "^20.1.0",
46
+ "@commitlint/config-conventional": "^20.0.0",
47
+ "@prisma/client": "^6.18.0",
48
+ "@tilli-pro/biome": "^0.8.0",
49
+ "@types/node": "^24.10.0",
50
+ "lefthook": "^2.0.2",
51
+ "pkgroll": "^2.20.1",
52
+ "prisma": "^6.18.0",
53
+ "tsx": "^4.20.6",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^2.1.9"
56
+ },
57
+ "scripts": {
58
+ "build": "pkgroll",
59
+ "dev": "pkgroll --watch",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest",
62
+ "test:e2e": "pnpm build && prisma generate && vitest run",
63
+ "pretest": "pnpm build",
64
+ "postinstall": "lefthook install",
65
+ "lint": "biome check",
66
+ "lint:fix": "biome check --write",
67
+ "changeset": "changeset",
68
+ "version": "changeset version",
69
+ "release": "pnpm build && changeset publish"
70
+ }
71
+ }