prisma-extension-autozod 1.0.4 → 1.1.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/README.md +136 -53
- package/dist/index.cjs +246 -33
- package/dist/index.d.cts +112 -15
- package/dist/index.d.ts +112 -15
- package/dist/index.js +240 -33
- package/package.json +9 -11
package/README.md
CHANGED
|
@@ -4,30 +4,44 @@
|
|
|
4
4
|
[](https://github.com/abzr0o/prisma-extension-autozod/actions)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
> Contract-aware compile-time linting and runtime Zod validation for the new Prisma ORM (@prisma/orm-postgres).
|
|
7
|
+
> Contract-aware compile-time linting, automatic schema generation, and runtime Zod validation for the new Prisma ORM (@prisma/orm-postgres).
|
|
8
8
|
|
|
9
|
-
`prisma-extension-autozod` connects **Zod 4** and **Prisma ORM 8**. It provides
|
|
9
|
+
`prisma-extension-autozod` connects **Zod 4** and **Prisma ORM 8**. It provides three layers of safety:
|
|
10
10
|
|
|
11
|
-
1. **
|
|
12
|
-
2. **
|
|
11
|
+
1. **Automatic Schema Inference**: Generate complete insert and update Zod schemas directly from `contract.json` without drifting field definitions.
|
|
12
|
+
2. **Compile-time Linting**: TypeScript flags any field in your Zod schema that does not exist in your Prisma data contract.
|
|
13
|
+
3. **Runtime Middleware**: Automatically intercepts write operations (`create`, `update`, `insert`) via Prisma 8's `beforeCompile` AST hook, maps domain fields to storage columns, sanitizes payloads, and writes back validated transforms, coercions, and defaults directly into database query parameters.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why prisma-extension-autozod?
|
|
18
|
+
|
|
19
|
+
| Feature | Manual Controller Validation | Schema Generators (zod-prisma) | prisma-extension-autozod |
|
|
20
|
+
| :--- | :--- | :--- | :--- |
|
|
21
|
+
| **Zero Controller Boilerplate** | No (Manual `.parse()`) | No (Manual `.parse()`) | Yes (Auto-intercepted) |
|
|
22
|
+
| **Prisma 8 Native Architecture** | No | No | Yes (`beforeCompile` / Contract) |
|
|
23
|
+
| **Direct Schema Inference** | No | Yes (Build step) | Yes (`createInsertSchema(contract, "User")`) |
|
|
24
|
+
| **Compile-Time Typo Linter** | No | No | Yes (Immediate IDE feedback) |
|
|
25
|
+
| **Casing & Column Mapping** | No | No | Yes (`first_name` <-> `firstName`) |
|
|
26
|
+
| **AST Value Write-Back** | No | No | Yes (Transforms and defaults reach DB) |
|
|
13
27
|
|
|
14
28
|
---
|
|
15
29
|
|
|
16
30
|
## Requirements
|
|
17
31
|
|
|
18
|
-
- **Node.js**: `>=22.0.0`
|
|
32
|
+
- **Node.js**: `>= 22.0.0`
|
|
19
33
|
- **Prisma ORM**: `@prisma/orm-postgres >= 8.0.0`
|
|
20
|
-
- **Zod**:
|
|
34
|
+
- **Zod**: `^4.0.0`
|
|
21
35
|
|
|
22
36
|
---
|
|
23
37
|
|
|
24
38
|
## Features
|
|
25
39
|
|
|
40
|
+
- **Direct Contract Schema Inference**: Infer base Zod schemas straight from your Prisma 8 `contract.json` via `createInsertSchema` and `createUpdateSchema`, with database defaults automatically made optional.
|
|
41
|
+
- **AST Transform & Default Write-Back**: Transformed and coerced values (e.g. `.trim().toLowerCase()`, `z.coerce.date()`, `.default()`) are updated in-place on AST parameter references so they actually persist to PostgreSQL.
|
|
42
|
+
- **Multi-Row Insert Protection**: Transparently handles ORM-generated `DefaultValueExpr` nodes in multi-row inserts without triggering false validation rejections.
|
|
43
|
+
- **Bidirectional Column & Model Mapping**: Seamlessly resolves domain model names to physical storage tables (e.g. `User` -> `users`) and model field names to storage columns (e.g. `firstName` -> `first_name`).
|
|
26
44
|
- **Compile-Time Contract Linting**: Catches schema mismatches and invalid fields at build-time with clear TypeScript diagnostics.
|
|
27
|
-
- **Native Prisma ORM 8 Lifecycle Hook**: Hooks directly into the `beforeCompile` AST pipeline without monkey-patching.
|
|
28
|
-
- **Automatic Column Normalization**: Remaps input casing (e.g., `first_name` to `firstName`) based on your contract's column catalog.
|
|
29
|
-
- **Context-Aware Validation**: Full validation for `create`/`insert`, and automatic `.partial()` validation for `update` operations so omitted fields do not fail.
|
|
30
|
-
- **Flexible Strictness**: Choose between `createZodRegistry` (allows omitting auto-generated/default fields like `id` or `createdAt`) and `createStrictZodRegistry` (enforces full table column coverage).
|
|
31
45
|
- **Dual ESM & CJS**: Full TypeScript `.d.ts` declarations included out-of-the-box.
|
|
32
46
|
|
|
33
47
|
---
|
|
@@ -49,51 +63,58 @@ yarn add prisma-extension-autozod zod @prisma/orm-postgres
|
|
|
49
63
|
|
|
50
64
|
## Quickstart
|
|
51
65
|
|
|
52
|
-
### 1.
|
|
66
|
+
### 1. Infer Schemas Directly from contract.json (Recommended)
|
|
67
|
+
|
|
68
|
+
Generate complete Zod schemas with zero manual field maintenance and customize individual fields with refinements:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { createInsertSchema, createUpdateSchema } from "prisma-extension-autozod";
|
|
72
|
+
import contractJson from "./contract.json" with { type: "json" };
|
|
73
|
+
|
|
74
|
+
// Inferred from contract.json with defaults (e.g. id, createdAt) marked optional:
|
|
75
|
+
export const userCreateSchema = createInsertSchema(contractJson, "User", {
|
|
76
|
+
email: (s) => s.email("Must be a valid email").toLowerCase().trim(),
|
|
77
|
+
name: (s) => s.min(2),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Update schema with all fields partial:
|
|
81
|
+
export const userUpdateSchema = createUpdateSchema(contractJson, "User");
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### 2. Or Define Custom Schemas with Contract Linting
|
|
53
87
|
|
|
54
|
-
|
|
88
|
+
If you prefer authoring your schemas manually, bind them to your contract types using `createZodRegistry`:
|
|
55
89
|
|
|
56
90
|
```typescript
|
|
57
91
|
import { z } from "zod";
|
|
58
92
|
import { createZodRegistry } from "prisma-extension-autozod";
|
|
59
93
|
import type { models } from "./contract.d.ts";
|
|
60
94
|
|
|
61
|
-
// Create a registry bound to your Prisma 8 contract models
|
|
62
95
|
const defineModel = createZodRegistry<typeof models.public>();
|
|
63
96
|
|
|
64
97
|
export const userValidator = defineModel(
|
|
65
98
|
"User",
|
|
66
99
|
z.object({
|
|
67
|
-
email: z.string().email(
|
|
68
|
-
username: z
|
|
69
|
-
|
|
70
|
-
.min(2, "Username must be at least 2 characters")
|
|
71
|
-
.nullable()
|
|
72
|
-
.optional(),
|
|
73
|
-
name: z
|
|
74
|
-
.string()
|
|
75
|
-
.min(2, "Name must be at least 2 characters")
|
|
76
|
-
.nullable()
|
|
77
|
-
.optional(),
|
|
100
|
+
email: z.string().email(),
|
|
101
|
+
username: z.string().min(2).nullable().optional(),
|
|
102
|
+
name: z.string().min(2).nullable().optional(),
|
|
78
103
|
}),
|
|
79
104
|
);
|
|
80
105
|
```
|
|
81
106
|
|
|
82
|
-
> **Compile-Time
|
|
107
|
+
> **Compile-Time Typo Protection:**
|
|
83
108
|
> If you add a field that does not exist in the contract:
|
|
84
|
-
>
|
|
85
109
|
> ```typescript
|
|
86
|
-
> defineModel(
|
|
87
|
-
>
|
|
88
|
-
>
|
|
89
|
-
> nonExistentColumn: z.string(), // Error: Field 'nonExistentColumn' does not exist in table contract
|
|
90
|
-
> }),
|
|
91
|
-
> );
|
|
110
|
+
> defineModel("User", z.object({
|
|
111
|
+
> nonExistentColumn: z.string(), // Error: Field 'nonExistentColumn' does not exist in table contract
|
|
112
|
+
> }));
|
|
92
113
|
> ```
|
|
93
114
|
|
|
94
115
|
---
|
|
95
116
|
|
|
96
|
-
###
|
|
117
|
+
### 3. Register Middleware in Your Prisma 8 Client
|
|
97
118
|
|
|
98
119
|
Pass the middleware to your `@prisma/orm-postgres` client setup:
|
|
99
120
|
|
|
@@ -103,7 +124,7 @@ import { contractFromJson } from "@prisma/orm-postgres/static";
|
|
|
103
124
|
import { autoZodPrisma8 } from "prisma-extension-autozod";
|
|
104
125
|
|
|
105
126
|
import contractJson from "./contract.json" with { type: "json" };
|
|
106
|
-
import {
|
|
127
|
+
import { userCreateSchema } from "./schemas.ts";
|
|
107
128
|
|
|
108
129
|
export const contract = contractFromJson(contractJson);
|
|
109
130
|
|
|
@@ -113,7 +134,7 @@ export const db = postgres({
|
|
|
113
134
|
middleware: [
|
|
114
135
|
autoZodPrisma8({
|
|
115
136
|
contract: contractJson,
|
|
116
|
-
validators: [
|
|
137
|
+
validators: [userCreateSchema],
|
|
117
138
|
}),
|
|
118
139
|
],
|
|
119
140
|
});
|
|
@@ -121,19 +142,19 @@ export const db = postgres({
|
|
|
121
142
|
|
|
122
143
|
---
|
|
123
144
|
|
|
124
|
-
###
|
|
145
|
+
### 4. Automatic Validation & Transform Write-Back
|
|
125
146
|
|
|
126
147
|
In the new Prisma ORM, models are accessed via `db.orm.<namespace>.<ModelName>`:
|
|
127
148
|
|
|
128
149
|
```typescript
|
|
129
|
-
// Valid: Passes validation and executes
|
|
150
|
+
// Valid: Passes validation, applies transforms, and executes
|
|
130
151
|
await db.orm.public.User.create({
|
|
131
|
-
email: "alice@prisma.io"
|
|
152
|
+
email: " Alice@Prisma.IO ", // Written back as "alice@prisma.io" in database
|
|
132
153
|
username: "alice",
|
|
133
154
|
name: "Alice Smith",
|
|
134
155
|
});
|
|
135
156
|
|
|
136
|
-
// Invalid: Throws standard ZodError before hitting
|
|
157
|
+
// Invalid: Throws standard ZodError before hitting PostgreSQL
|
|
137
158
|
await db.orm.public.User.create({
|
|
138
159
|
email: "not-an-email",
|
|
139
160
|
username: "a", // too short (< 2 characters)
|
|
@@ -142,7 +163,53 @@ await db.orm.public.User.create({
|
|
|
142
163
|
|
|
143
164
|
// Update: Only supplied fields are validated
|
|
144
165
|
await db.orm.public.User.where({ email: "alice@prisma.io" }).update({
|
|
145
|
-
name: "Alice Wonder", // Valid;
|
|
166
|
+
name: "Alice Wonder", // Valid; omitted fields are ignored on updates
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### 5. Safe Route Validation (Mass Assignment Protection)
|
|
173
|
+
|
|
174
|
+
Never pass raw client input (`req.body`) directly to database writes. In web applications, unvalidated payloads allow clients to inject unauthorized columns (such as `role: 'admin'`, `emailVerified: true`, or `ownerId`).
|
|
175
|
+
|
|
176
|
+
The recommended, secure pattern is to parse `req.body` at your route handler, which strips unauthorized fields, while `autoZodPrisma8` provides an automatic database-layer guard rail:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { ZodError } from "zod";
|
|
180
|
+
import { userCreateSchema } from "./schemas.ts";
|
|
181
|
+
|
|
182
|
+
// Express / NestJS / Fastify / Hono route handler:
|
|
183
|
+
app.post("/users", async (req, res) => {
|
|
184
|
+
try {
|
|
185
|
+
// 1. Safe route validation: strips unknown fields & prevents mass assignment
|
|
186
|
+
const validatedData = userCreateSchema.parse(req.body);
|
|
187
|
+
|
|
188
|
+
// 2. Database mutation: intercepted & verified by AutoZod middleware
|
|
189
|
+
const user = await db.orm.public.User.create(validatedData);
|
|
190
|
+
return res.status(201).json(user);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof ZodError) {
|
|
193
|
+
// Return clean, flattened validation errors directly to the client
|
|
194
|
+
return res.status(400).json({ errors: error.flatten().fieldErrors });
|
|
195
|
+
}
|
|
196
|
+
return res.status(500).json({ error: "Internal server error" });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### 6. Storage Column & Casing Normalization
|
|
204
|
+
|
|
205
|
+
If your Prisma contract maps camelCase model fields to snake_case storage columns (e.g. `@map("first_name")`), or if incoming payloads use alternative casing, `autoZodPrisma8` transparently reconciles them:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// Model has firstName with @map("first_name"):
|
|
209
|
+
// Middleware automatically resolves `first_name` in AST rows to the `firstName` schema
|
|
210
|
+
await db.orm.public.User.create({
|
|
211
|
+
first_name: "Alice",
|
|
212
|
+
email: "alice@prisma.io",
|
|
146
213
|
});
|
|
147
214
|
```
|
|
148
215
|
|
|
@@ -150,10 +217,25 @@ await db.orm.public.User.where({ email: "alice@prisma.io" }).update({
|
|
|
150
217
|
|
|
151
218
|
## API Reference
|
|
152
219
|
|
|
220
|
+
### `createInsertSchema(contract, modelName, refinements?)`
|
|
221
|
+
|
|
222
|
+
Infers a complete Zod insert schema from `contract.json`. Fields with database defaults (`autoincrement()`, `now()`) are marked optional.
|
|
223
|
+
```typescript
|
|
224
|
+
const userCreateSchema = createInsertSchema(contractJson, "User", {
|
|
225
|
+
email: (s) => s.email(),
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### `createUpdateSchema(contract, modelName, refinements?)`
|
|
230
|
+
|
|
231
|
+
Infers a partial Zod update schema from `contract.json`.
|
|
232
|
+
```typescript
|
|
233
|
+
const userUpdateSchema = createUpdateSchema(contractJson, "User");
|
|
234
|
+
```
|
|
235
|
+
|
|
153
236
|
### `createZodRegistry<TModels>()`
|
|
154
237
|
|
|
155
238
|
Creates a model validator factory bound to your contract models. Validates that all fields in the schema exist in the contract table, allowing omission of auto-generated or optional fields.
|
|
156
|
-
|
|
157
239
|
```typescript
|
|
158
240
|
const defineModel = createZodRegistry<typeof models.public>();
|
|
159
241
|
const userValidator = defineModel("User", zodSchema);
|
|
@@ -162,7 +244,6 @@ const userValidator = defineModel("User", zodSchema);
|
|
|
162
244
|
### `createStrictZodRegistry<TModels>()`
|
|
163
245
|
|
|
164
246
|
Creates a strict validator factory that requires all columns in the contract table to be declared in the schema, while also rejecting unknown fields.
|
|
165
|
-
|
|
166
247
|
```typescript
|
|
167
248
|
const defineStrict = createStrictZodRegistry<typeof models.public>();
|
|
168
249
|
const userValidator = defineStrict("User", zodSchema);
|
|
@@ -171,17 +252,8 @@ const userValidator = defineStrict("User", zodSchema);
|
|
|
171
252
|
### `autoZodPrisma8(options)` (alias: `autoZod`)
|
|
172
253
|
|
|
173
254
|
Prisma ORM 8 middleware factory:
|
|
174
|
-
|
|
175
|
-
- `options.
|
|
176
|
-
- `options.validators`: Array of validator entries returned by `createZodRegistry` or `createStrictZodRegistry`.
|
|
177
|
-
|
|
178
|
-
### `extractColumns(contractJson)`
|
|
179
|
-
|
|
180
|
-
Helper function that inspects storage namespaces inside `contract.json` and returns a normalized dictionary of table column names.
|
|
181
|
-
|
|
182
|
-
### `normalizeKey(key: string)`
|
|
183
|
-
|
|
184
|
-
Normalizes strings to lowercase alphanumeric format for reliable case- and separator-insensitive mapping.
|
|
255
|
+
- `options.contract`: The parsed `contract.json` object.
|
|
256
|
+
- `options.validators`: Array of validator entries returned by `createInsertSchema`, `createZodRegistry`, or `createStrictZodRegistry`.
|
|
185
257
|
|
|
186
258
|
---
|
|
187
259
|
|
|
@@ -203,6 +275,17 @@ pnpm run build
|
|
|
203
275
|
|
|
204
276
|
---
|
|
205
277
|
|
|
278
|
+
## Contributing
|
|
279
|
+
|
|
280
|
+
Contributions, bug reports, and feature requests are welcome!
|
|
281
|
+
Feel free to check the [issues page](https://github.com/abzr0o/prisma-extension-autozod/issues).
|
|
282
|
+
|
|
283
|
+
## Support
|
|
284
|
+
|
|
285
|
+
If this library saves you time or prevents schema bugs in production, consider giving it a star on [GitHub](https://github.com/abzr0o/prisma-extension-autozod)!
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
206
289
|
## License
|
|
207
290
|
|
|
208
291
|
[MIT](LICENSE) (c) [Mohammed Thamer](https://github.com/abzr0o)
|
package/dist/index.cjs
CHANGED
|
@@ -22,9 +22,15 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
autoZod: () => autoZod,
|
|
24
24
|
autoZodPrisma8: () => autoZodPrisma8,
|
|
25
|
+
codecToZodSchema: () => codecToZodSchema,
|
|
26
|
+
createInsertSchema: () => createInsertSchema,
|
|
25
27
|
createStrictZodRegistry: () => createStrictZodRegistry,
|
|
28
|
+
createUpdateSchema: () => createUpdateSchema,
|
|
26
29
|
createZodRegistry: () => createZodRegistry,
|
|
27
30
|
extractColumns: () => extractColumns,
|
|
31
|
+
extractContractMetadata: () => extractContractMetadata,
|
|
32
|
+
extractValue: () => extractValue,
|
|
33
|
+
isDefaultValueExpr: () => isDefaultValueExpr,
|
|
28
34
|
normalizeKey: () => normalizeKey
|
|
29
35
|
});
|
|
30
36
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -33,68 +39,200 @@ module.exports = __toCommonJS(index_exports);
|
|
|
33
39
|
function normalizeKey(key) {
|
|
34
40
|
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
35
41
|
}
|
|
42
|
+
function isDefaultValueExpr(val) {
|
|
43
|
+
if (!val || typeof val !== "object") return false;
|
|
44
|
+
return val.kind === "default-value" || val.constructor?.name === "DefaultValueExpr";
|
|
45
|
+
}
|
|
36
46
|
function extractValue(val) {
|
|
47
|
+
if (isDefaultValueExpr(val)) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
37
50
|
if (val && typeof val === "object" && "value" in val) {
|
|
38
51
|
return val.value;
|
|
39
52
|
}
|
|
40
53
|
return val;
|
|
41
54
|
}
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
55
|
+
function extractContractMetadata(contractJson) {
|
|
56
|
+
const modelsByName = /* @__PURE__ */ new Map();
|
|
57
|
+
const modelsByTable = /* @__PURE__ */ new Map();
|
|
58
|
+
const columnsByTable = {};
|
|
59
|
+
const domainNamespaces = contractJson?.domain?.namespaces || {};
|
|
60
|
+
const storageNamespaces = contractJson?.storage?.namespaces || {};
|
|
61
|
+
const storageDefaults = {};
|
|
62
|
+
for (const [, ns] of Object.entries(storageNamespaces)) {
|
|
46
63
|
const tables = ns?.entries?.table || ns?.tables || {};
|
|
47
|
-
for (const [
|
|
48
|
-
|
|
64
|
+
for (const [tableName, tableDef] of Object.entries(tables)) {
|
|
65
|
+
const cleanTable = normalizeKey(tableName);
|
|
66
|
+
const cols = Object.keys(tableDef?.columns || {});
|
|
67
|
+
columnsByTable[cleanTable] = cols;
|
|
68
|
+
storageDefaults[cleanTable] = {};
|
|
69
|
+
for (const [colName, colDef] of Object.entries(
|
|
70
|
+
tableDef?.columns || {}
|
|
71
|
+
)) {
|
|
72
|
+
storageDefaults[cleanTable][normalizeKey(colName)] = colDef?.default !== void 0;
|
|
73
|
+
}
|
|
49
74
|
}
|
|
50
75
|
}
|
|
51
|
-
|
|
76
|
+
for (const [nsName, ns] of Object.entries(domainNamespaces)) {
|
|
77
|
+
const models = ns?.models || {};
|
|
78
|
+
for (const [modelName, modelDef] of Object.entries(models)) {
|
|
79
|
+
const storageTable = modelDef?.storage?.table || modelName;
|
|
80
|
+
const storageNs = modelDef?.storage?.namespaceId || nsName;
|
|
81
|
+
const cleanModelName = normalizeKey(modelName);
|
|
82
|
+
const cleanStorageTable = normalizeKey(storageTable);
|
|
83
|
+
const modelFieldToColumn = /* @__PURE__ */ new Map();
|
|
84
|
+
const columnToModelField = /* @__PURE__ */ new Map();
|
|
85
|
+
const fields = {};
|
|
86
|
+
const domainFields = modelDef?.fields || {};
|
|
87
|
+
const storageFields = modelDef?.storage?.fields || {};
|
|
88
|
+
for (const [fieldName, fieldDef] of Object.entries(domainFields)) {
|
|
89
|
+
const storageCol = storageFields[fieldName]?.column || fieldName;
|
|
90
|
+
const cleanFieldName = normalizeKey(fieldName);
|
|
91
|
+
const cleanColName = normalizeKey(storageCol);
|
|
92
|
+
modelFieldToColumn.set(cleanFieldName, storageCol);
|
|
93
|
+
modelFieldToColumn.set(cleanColName, storageCol);
|
|
94
|
+
columnToModelField.set(cleanColName, fieldName);
|
|
95
|
+
columnToModelField.set(cleanFieldName, fieldName);
|
|
96
|
+
const hasDefault = Boolean(
|
|
97
|
+
storageDefaults[cleanStorageTable]?.[cleanColName]
|
|
98
|
+
);
|
|
99
|
+
fields[fieldName] = {
|
|
100
|
+
modelFieldName: fieldName,
|
|
101
|
+
storageColumn: storageCol,
|
|
102
|
+
nullable: Boolean(fieldDef?.nullable),
|
|
103
|
+
codecId: fieldDef?.type?.codecId,
|
|
104
|
+
hasDefault
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const meta = {
|
|
108
|
+
modelName,
|
|
109
|
+
storageTable,
|
|
110
|
+
storageNamespace: storageNs,
|
|
111
|
+
modelFieldToColumn,
|
|
112
|
+
columnToModelField,
|
|
113
|
+
fields
|
|
114
|
+
};
|
|
115
|
+
modelsByName.set(cleanModelName, meta);
|
|
116
|
+
modelsByTable.set(cleanStorageTable, meta);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { modelsByName, modelsByTable, columnsByTable };
|
|
120
|
+
}
|
|
121
|
+
function extractColumns(contractJson) {
|
|
122
|
+
const metadata = extractContractMetadata(contractJson);
|
|
123
|
+
return metadata.columnsByTable;
|
|
52
124
|
}
|
|
53
125
|
function autoZodPrisma8(options) {
|
|
54
|
-
const
|
|
126
|
+
const contractMeta = extractContractMetadata(options.contract);
|
|
55
127
|
const validatorMap = /* @__PURE__ */ new Map();
|
|
56
128
|
for (const entry of options.validators) {
|
|
57
|
-
|
|
129
|
+
const rawName = entry.tableName || entry._tableName || entry.name;
|
|
130
|
+
const schema = entry.schema || entry;
|
|
131
|
+
if (rawName && schema) {
|
|
132
|
+
const cleanName = normalizeKey(rawName);
|
|
133
|
+
validatorMap.set(cleanName, schema);
|
|
134
|
+
const meta = contractMeta.modelsByName.get(cleanName) || contractMeta.modelsByTable.get(cleanName);
|
|
135
|
+
if (meta) {
|
|
136
|
+
validatorMap.set(normalizeKey(meta.modelName), schema);
|
|
137
|
+
validatorMap.set(normalizeKey(meta.storageTable), schema);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
58
140
|
}
|
|
59
141
|
return {
|
|
60
142
|
name: "autozod-validator",
|
|
61
143
|
familyId: "sql",
|
|
62
144
|
async beforeCompile(draft, ctx) {
|
|
63
145
|
const ast = draft?.ast;
|
|
64
|
-
if (!ast || ast.kind !== "insert" && ast.kind !== "update")
|
|
65
|
-
|
|
66
|
-
if (!target) return void 0;
|
|
67
|
-
const cleanTarget = normalizeKey(target);
|
|
68
|
-
const schema = validatorMap.get(cleanTarget);
|
|
69
|
-
if (!schema) return void 0;
|
|
70
|
-
const targetCols = columnCatalog[cleanTarget] || [];
|
|
71
|
-
const colLookup = /* @__PURE__ */ new Map();
|
|
72
|
-
for (const col of targetCols) {
|
|
73
|
-
colLookup.set(normalizeKey(col), col);
|
|
146
|
+
if (!ast || ast.kind !== "insert" && ast.kind !== "update") {
|
|
147
|
+
return void 0;
|
|
74
148
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
149
|
+
const rawTable = ast.table?.name;
|
|
150
|
+
if (!rawTable) return void 0;
|
|
151
|
+
const cleanTable = normalizeKey(rawTable);
|
|
152
|
+
const meta = contractMeta.modelsByTable.get(cleanTable) || contractMeta.modelsByName.get(cleanTable);
|
|
153
|
+
const schema = validatorMap.get(cleanTable) || (meta ? validatorMap.get(normalizeKey(meta.modelName)) : void 0);
|
|
154
|
+
if (!schema) return void 0;
|
|
155
|
+
const schemaShape = schema.shape || {};
|
|
156
|
+
const schemaKeys = Object.keys(schemaShape);
|
|
157
|
+
const validateAndWriteBack = (rawRecord, isUpdate) => {
|
|
158
|
+
const toValidate = {};
|
|
159
|
+
const defaultExprFields = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const expectedKey of schemaKeys) {
|
|
161
|
+
if (expectedKey in rawRecord) {
|
|
162
|
+
const raw = rawRecord[expectedKey];
|
|
163
|
+
if (isDefaultValueExpr(raw)) {
|
|
164
|
+
defaultExprFields.add(expectedKey);
|
|
165
|
+
} else {
|
|
166
|
+
toValidate[expectedKey] = extractValue(raw);
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const storageCol = meta?.modelFieldToColumn.get(normalizeKey(expectedKey));
|
|
171
|
+
if (storageCol && storageCol in rawRecord) {
|
|
172
|
+
const raw = rawRecord[storageCol];
|
|
173
|
+
if (isDefaultValueExpr(raw)) {
|
|
174
|
+
defaultExprFields.add(expectedKey);
|
|
175
|
+
} else {
|
|
176
|
+
toValidate[expectedKey] = extractValue(raw);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
77
181
|
for (const [col, val] of Object.entries(rawRecord)) {
|
|
78
|
-
const
|
|
79
|
-
|
|
182
|
+
const mappedModelField = meta?.columnToModelField.get(
|
|
183
|
+
normalizeKey(col)
|
|
184
|
+
);
|
|
185
|
+
if (mappedModelField && mappedModelField in schemaShape) {
|
|
186
|
+
if (isDefaultValueExpr(val)) {
|
|
187
|
+
defaultExprFields.add(mappedModelField);
|
|
188
|
+
} else if (!(mappedModelField in toValidate)) {
|
|
189
|
+
toValidate[mappedModelField] = extractValue(val);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
80
192
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
193
|
+
let activeSchema = isUpdate ? schema.partial() : schema;
|
|
194
|
+
if (!isUpdate && defaultExprFields.size > 0 && typeof schema.partial === "function") {
|
|
195
|
+
const mask = {};
|
|
196
|
+
for (const f of defaultExprFields) {
|
|
197
|
+
mask[f] = true;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
activeSchema = schema.partial(mask);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const parsed = activeSchema.parse(toValidate);
|
|
205
|
+
if (parsed && typeof parsed === "object") {
|
|
206
|
+
for (const [parsedKey, parsedVal] of Object.entries(parsed)) {
|
|
207
|
+
let storageCol = meta?.modelFieldToColumn.get(
|
|
208
|
+
normalizeKey(parsedKey)
|
|
209
|
+
);
|
|
210
|
+
if (!storageCol || !(storageCol in rawRecord)) {
|
|
211
|
+
if (parsedKey in rawRecord) {
|
|
212
|
+
storageCol = parsedKey;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (storageCol && storageCol in rawRecord) {
|
|
216
|
+
const current = rawRecord[storageCol];
|
|
217
|
+
if (current && typeof current === "object" && "value" in current) {
|
|
218
|
+
current.value = parsedVal;
|
|
219
|
+
} else if (isDefaultValueExpr(current)) {
|
|
220
|
+
rawRecord[storageCol] = { value: parsedVal };
|
|
221
|
+
} else {
|
|
222
|
+
rawRecord[storageCol] = parsedVal;
|
|
223
|
+
}
|
|
224
|
+
} else if (storageCol && !isUpdate) {
|
|
225
|
+
rawRecord[storageCol] = { value: parsedVal };
|
|
226
|
+
}
|
|
87
227
|
}
|
|
88
228
|
}
|
|
89
|
-
const activeSchema = isUpdate ? schema.partial() : schema;
|
|
90
|
-
activeSchema.parse(toValidate);
|
|
91
229
|
};
|
|
92
230
|
if (ast.kind === "insert" && Array.isArray(ast.rows)) {
|
|
93
231
|
for (const row of ast.rows) {
|
|
94
|
-
|
|
232
|
+
validateAndWriteBack(row, false);
|
|
95
233
|
}
|
|
96
234
|
} else if (ast.kind === "update" && ast.set) {
|
|
97
|
-
|
|
235
|
+
validateAndWriteBack(ast.set, true);
|
|
98
236
|
}
|
|
99
237
|
return void 0;
|
|
100
238
|
}
|
|
@@ -119,12 +257,87 @@ function createStrictZodRegistry() {
|
|
|
119
257
|
};
|
|
120
258
|
};
|
|
121
259
|
}
|
|
260
|
+
|
|
261
|
+
// src/schema-generator.ts
|
|
262
|
+
var import_zod = require("zod");
|
|
263
|
+
function codecToZodSchema(codecId) {
|
|
264
|
+
if (!codecId) return import_zod.z.any();
|
|
265
|
+
const id = codecId.toLowerCase();
|
|
266
|
+
if (id.includes("uuid")) {
|
|
267
|
+
return import_zod.z.string().uuid();
|
|
268
|
+
}
|
|
269
|
+
if (id.includes("/text") || id.includes("/varchar") || id.includes("/char") || id.includes("/citext")) {
|
|
270
|
+
return import_zod.z.string();
|
|
271
|
+
}
|
|
272
|
+
if (id.includes("/int2") || id.includes("/int4")) {
|
|
273
|
+
return import_zod.z.number().int();
|
|
274
|
+
}
|
|
275
|
+
if (id.includes("/float") || id.includes("/numeric")) {
|
|
276
|
+
return import_zod.z.number();
|
|
277
|
+
}
|
|
278
|
+
if (id.includes("/int8")) {
|
|
279
|
+
return import_zod.z.union([import_zod.z.bigint(), import_zod.z.number()]);
|
|
280
|
+
}
|
|
281
|
+
if (id.includes("/bool")) {
|
|
282
|
+
return import_zod.z.boolean();
|
|
283
|
+
}
|
|
284
|
+
if (id.includes("/time") || id.includes("/date")) {
|
|
285
|
+
return import_zod.z.union([import_zod.z.date(), import_zod.z.string()]);
|
|
286
|
+
}
|
|
287
|
+
if (id.includes("/json")) {
|
|
288
|
+
return import_zod.z.any();
|
|
289
|
+
}
|
|
290
|
+
if (id.includes("/bytea")) {
|
|
291
|
+
return import_zod.z.union([import_zod.z.instanceof(Uint8Array), import_zod.z.string()]);
|
|
292
|
+
}
|
|
293
|
+
return import_zod.z.any();
|
|
294
|
+
}
|
|
295
|
+
function createInsertSchema(contract, modelName, refinements) {
|
|
296
|
+
const metadata = extractContractMetadata(contract);
|
|
297
|
+
const modelMeta = metadata.modelsByName.get(normalizeKey(modelName)) || metadata.modelsByTable.get(normalizeKey(modelName));
|
|
298
|
+
if (!modelMeta) {
|
|
299
|
+
const available = Array.from(metadata.modelsByName.values()).map((m) => m.modelName).join(", ");
|
|
300
|
+
throw new Error(
|
|
301
|
+
`[prisma-extension-autozod]: Model '${modelName}' not found in contract. Available models: ${available || "none"}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const shape = {};
|
|
305
|
+
for (const [fieldName, fieldMeta] of Object.entries(modelMeta.fields)) {
|
|
306
|
+
let fieldSchema = codecToZodSchema(fieldMeta.codecId);
|
|
307
|
+
if (fieldMeta.nullable) {
|
|
308
|
+
fieldSchema = fieldSchema.nullable();
|
|
309
|
+
}
|
|
310
|
+
if (fieldMeta.hasDefault) {
|
|
311
|
+
fieldSchema = fieldSchema.optional();
|
|
312
|
+
}
|
|
313
|
+
const custom = refinements?.[fieldName] || refinements?.[fieldMeta.storageColumn];
|
|
314
|
+
if (custom) {
|
|
315
|
+
fieldSchema = typeof custom === "function" ? custom(fieldSchema) : custom;
|
|
316
|
+
}
|
|
317
|
+
shape[fieldName] = fieldSchema;
|
|
318
|
+
}
|
|
319
|
+
const schema = import_zod.z.object(shape);
|
|
320
|
+
schema.tableName = modelName;
|
|
321
|
+
return schema;
|
|
322
|
+
}
|
|
323
|
+
function createUpdateSchema(contract, modelName, refinements) {
|
|
324
|
+
const insertSchema = createInsertSchema(contract, modelName, refinements);
|
|
325
|
+
const updateSchema = insertSchema.partial();
|
|
326
|
+
updateSchema.tableName = modelName;
|
|
327
|
+
return updateSchema;
|
|
328
|
+
}
|
|
122
329
|
// Annotate the CommonJS export names for ESM import in node:
|
|
123
330
|
0 && (module.exports = {
|
|
124
331
|
autoZod,
|
|
125
332
|
autoZodPrisma8,
|
|
333
|
+
codecToZodSchema,
|
|
334
|
+
createInsertSchema,
|
|
126
335
|
createStrictZodRegistry,
|
|
336
|
+
createUpdateSchema,
|
|
127
337
|
createZodRegistry,
|
|
128
338
|
extractColumns,
|
|
339
|
+
extractContractMetadata,
|
|
340
|
+
extractValue,
|
|
341
|
+
isDefaultValueExpr,
|
|
129
342
|
normalizeKey
|
|
130
343
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -122,7 +122,7 @@ interface AutoZodMiddleware {
|
|
|
122
122
|
familyId: "sql";
|
|
123
123
|
/**
|
|
124
124
|
* Lifecycle hook invoked prior to SQL plan compilation.
|
|
125
|
-
* Intercepts and
|
|
125
|
+
* Intercepts, validates, and writes back AST mutation rows and values.
|
|
126
126
|
*/
|
|
127
127
|
beforeCompile(draft: any, ctx: any): Promise<undefined>;
|
|
128
128
|
}
|
|
@@ -140,31 +140,71 @@ interface AutoZodMiddleware {
|
|
|
140
140
|
* ```
|
|
141
141
|
*/
|
|
142
142
|
declare function normalizeKey(key: string): string;
|
|
143
|
+
/**
|
|
144
|
+
* Checks whether an AST node is a Prisma 8 DefaultValueExpr placeholder
|
|
145
|
+
* inserted during multi-row inserts with asymmetric keys.
|
|
146
|
+
*/
|
|
147
|
+
declare function isDefaultValueExpr(val: unknown): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Extracts the raw JavaScript value from Prisma 8 AST parameter references.
|
|
150
|
+
* Automatically returns undefined for DefaultValueExpr nodes so Zod does not fail on them.
|
|
151
|
+
*/
|
|
152
|
+
declare function extractValue(val: unknown): unknown;
|
|
153
|
+
/**
|
|
154
|
+
* Metadata for a model field extracted from contract.json.
|
|
155
|
+
*/
|
|
156
|
+
interface ModelFieldMeta {
|
|
157
|
+
modelFieldName: string;
|
|
158
|
+
storageColumn: string;
|
|
159
|
+
nullable: boolean;
|
|
160
|
+
codecId?: string;
|
|
161
|
+
hasDefault: boolean;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Metadata for a domain model extracted from contract.json.
|
|
165
|
+
*/
|
|
166
|
+
interface ModelContractMeta {
|
|
167
|
+
modelName: string;
|
|
168
|
+
storageTable: string;
|
|
169
|
+
storageNamespace: string;
|
|
170
|
+
modelFieldToColumn: Map<string, string>;
|
|
171
|
+
columnToModelField: Map<string, string>;
|
|
172
|
+
fields: Record<string, ModelFieldMeta>;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Comprehensive metadata extracted from contract.json across all namespaces.
|
|
176
|
+
*/
|
|
177
|
+
interface ContractMetadata {
|
|
178
|
+
modelsByName: Map<string, ModelContractMeta>;
|
|
179
|
+
modelsByTable: Map<string, ModelContractMeta>;
|
|
180
|
+
columnsByTable: Record<string, string[]>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Inspects a compiled Prisma ORM 8 `contract.json` object and extracts
|
|
184
|
+
* bidirectional mappings between domain models, storage tables, field names, and column names.
|
|
185
|
+
*
|
|
186
|
+
* @param contractJson The parsed Prisma 8 contract JSON object.
|
|
187
|
+
* @returns A ContractMetadata object with indexed model and column lookups.
|
|
188
|
+
*/
|
|
189
|
+
declare function extractContractMetadata(contractJson: any): ContractMetadata;
|
|
143
190
|
/**
|
|
144
191
|
* Inspects a compiled Prisma ORM 8 `contract.json` object and extracts a
|
|
145
192
|
* dictionary of normalized table names mapped to their column list.
|
|
146
193
|
*
|
|
147
194
|
* @param contractJson The parsed Prisma 8 contract JSON object.
|
|
148
195
|
* @returns A mapping from normalized table names to array of column names.
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```typescript
|
|
152
|
-
* import contractJson from "./contract.json" with { type: "json" };
|
|
153
|
-
*
|
|
154
|
-
* const columns = extractColumns(contractJson);
|
|
155
|
-
* console.log(columns["user"]); // ["id", "email", "name", ...]
|
|
156
|
-
* ```
|
|
157
196
|
*/
|
|
158
197
|
declare function extractColumns(contractJson: any): Record<string, string[]>;
|
|
159
198
|
/**
|
|
160
199
|
* Factory creating Prisma ORM 8 SQL middleware for automatic runtime Zod validation.
|
|
161
200
|
*
|
|
162
201
|
* Hooks into the query compiler via `beforeCompile` and intercepts `insert` and `update`
|
|
163
|
-
* operations.
|
|
164
|
-
*
|
|
202
|
+
* operations. Resolves domain model names to physical storage tables, maps model field
|
|
203
|
+
* names to database columns, validates values, and writes back coerced, transformed,
|
|
204
|
+
* and defaulted values into the AST parameter references.
|
|
165
205
|
*
|
|
166
206
|
* @param options.contract The parsed Prisma 8 `contract.json` object.
|
|
167
|
-
* @param options.validators An array of validator entries
|
|
207
|
+
* @param options.validators An array of validator entries or schemas created by createZodRegistry / createInsertSchema.
|
|
168
208
|
* @returns An `AutoZodMiddleware` instance ready to be passed to `postgres({ middleware: [...] })`.
|
|
169
209
|
*
|
|
170
210
|
* @example
|
|
@@ -192,12 +232,69 @@ declare function extractColumns(contractJson: any): Record<string, string[]>;
|
|
|
192
232
|
*/
|
|
193
233
|
declare function autoZodPrisma8(options: {
|
|
194
234
|
contract: any;
|
|
195
|
-
validators: ModelValidatorEntry
|
|
235
|
+
validators: (ModelValidatorEntry | (z.ZodObject<any> & {
|
|
236
|
+
tableName?: string;
|
|
237
|
+
}))[];
|
|
196
238
|
}): AutoZodMiddleware;
|
|
197
239
|
/**
|
|
198
240
|
* Convenient alias for `autoZodPrisma8`.
|
|
199
241
|
*/
|
|
200
242
|
declare const autoZod: typeof autoZodPrisma8;
|
|
201
243
|
|
|
202
|
-
|
|
203
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Refinement mapping allowing custom Zod schemas or mutator functions per field.
|
|
246
|
+
*/
|
|
247
|
+
type SchemaRefinements = Record<string, z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny)>;
|
|
248
|
+
/**
|
|
249
|
+
* Map Prisma 8 SQL codec IDs to their corresponding base Zod types.
|
|
250
|
+
*/
|
|
251
|
+
declare function codecToZodSchema(codecId?: string): z.ZodTypeAny;
|
|
252
|
+
/**
|
|
253
|
+
* Automatically infers a complete Zod insert schema from a Prisma ORM 8 `contract.json`.
|
|
254
|
+
*
|
|
255
|
+
* Fields with database defaults (e.g. autoincrement ID, now() timestamps) are automatically
|
|
256
|
+
* marked optional. Nullable columns are marked nullable. You can refine or override individual
|
|
257
|
+
* fields with custom validation rules.
|
|
258
|
+
*
|
|
259
|
+
* @param contract The parsed Prisma 8 `contract.json` object.
|
|
260
|
+
* @param modelName The model name in your contract (e.g., "User" or "Post").
|
|
261
|
+
* @param refinements Optional mapping to customize or extend specific field schemas.
|
|
262
|
+
* @returns A ZodObject schema with a `.tableName` property ready for route validation or AutoZod middleware.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* import contractJson from "./contract.json" with { type: "json" };
|
|
267
|
+
* import { createInsertSchema } from "prisma-extension-autozod";
|
|
268
|
+
*
|
|
269
|
+
* export const userCreateSchema = createInsertSchema(contractJson, "User", {
|
|
270
|
+
* email: (s) => s.email("Must be a valid email"),
|
|
271
|
+
* name: (s) => s.min(2),
|
|
272
|
+
* });
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
declare function createInsertSchema<TContract extends Record<string, any> = any, TModel extends string = string>(contract: TContract, modelName: TModel, refinements?: SchemaRefinements): z.ZodObject<any> & {
|
|
276
|
+
tableName: TModel;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Automatically infers a Zod update schema from a Prisma ORM 8 `contract.json`.
|
|
280
|
+
*
|
|
281
|
+
* Equivalent to `createInsertSchema(...).partial()`.
|
|
282
|
+
*
|
|
283
|
+
* @param contract The parsed Prisma 8 `contract.json` object.
|
|
284
|
+
* @param modelName The model name in your contract.
|
|
285
|
+
* @param refinements Optional mapping to customize or extend specific field schemas.
|
|
286
|
+
* @returns A partial ZodObject schema with a `.tableName` property.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```typescript
|
|
290
|
+
* export const userUpdateSchema = createUpdateSchema(contractJson, "User", {
|
|
291
|
+
* email: (s) => s.email(),
|
|
292
|
+
* });
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
declare function createUpdateSchema<TContract extends Record<string, any> = any, TModel extends string = string>(contract: TContract, modelName: TModel, refinements?: SchemaRefinements): z.ZodObject<any> & {
|
|
296
|
+
tableName: TModel;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export { autoZod, autoZodPrisma8, codecToZodSchema, createInsertSchema, createStrictZodRegistry, createUpdateSchema, createZodRegistry, extractColumns, extractContractMetadata, extractValue, isDefaultValueExpr, normalizeKey };
|
|
300
|
+
export type { AutoZodMiddleware, CleanKey, ContractMetadata, InvalidContractKeys, MissingContractKeys, ModelContractMeta, ModelFieldMeta, ModelValidatorEntry, ResolveColumn, SchemaRefinements, ValidateZodAgainstContract };
|
package/dist/index.d.ts
CHANGED
|
@@ -122,7 +122,7 @@ interface AutoZodMiddleware {
|
|
|
122
122
|
familyId: "sql";
|
|
123
123
|
/**
|
|
124
124
|
* Lifecycle hook invoked prior to SQL plan compilation.
|
|
125
|
-
* Intercepts and
|
|
125
|
+
* Intercepts, validates, and writes back AST mutation rows and values.
|
|
126
126
|
*/
|
|
127
127
|
beforeCompile(draft: any, ctx: any): Promise<undefined>;
|
|
128
128
|
}
|
|
@@ -140,31 +140,71 @@ interface AutoZodMiddleware {
|
|
|
140
140
|
* ```
|
|
141
141
|
*/
|
|
142
142
|
declare function normalizeKey(key: string): string;
|
|
143
|
+
/**
|
|
144
|
+
* Checks whether an AST node is a Prisma 8 DefaultValueExpr placeholder
|
|
145
|
+
* inserted during multi-row inserts with asymmetric keys.
|
|
146
|
+
*/
|
|
147
|
+
declare function isDefaultValueExpr(val: unknown): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Extracts the raw JavaScript value from Prisma 8 AST parameter references.
|
|
150
|
+
* Automatically returns undefined for DefaultValueExpr nodes so Zod does not fail on them.
|
|
151
|
+
*/
|
|
152
|
+
declare function extractValue(val: unknown): unknown;
|
|
153
|
+
/**
|
|
154
|
+
* Metadata for a model field extracted from contract.json.
|
|
155
|
+
*/
|
|
156
|
+
interface ModelFieldMeta {
|
|
157
|
+
modelFieldName: string;
|
|
158
|
+
storageColumn: string;
|
|
159
|
+
nullable: boolean;
|
|
160
|
+
codecId?: string;
|
|
161
|
+
hasDefault: boolean;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Metadata for a domain model extracted from contract.json.
|
|
165
|
+
*/
|
|
166
|
+
interface ModelContractMeta {
|
|
167
|
+
modelName: string;
|
|
168
|
+
storageTable: string;
|
|
169
|
+
storageNamespace: string;
|
|
170
|
+
modelFieldToColumn: Map<string, string>;
|
|
171
|
+
columnToModelField: Map<string, string>;
|
|
172
|
+
fields: Record<string, ModelFieldMeta>;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Comprehensive metadata extracted from contract.json across all namespaces.
|
|
176
|
+
*/
|
|
177
|
+
interface ContractMetadata {
|
|
178
|
+
modelsByName: Map<string, ModelContractMeta>;
|
|
179
|
+
modelsByTable: Map<string, ModelContractMeta>;
|
|
180
|
+
columnsByTable: Record<string, string[]>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Inspects a compiled Prisma ORM 8 `contract.json` object and extracts
|
|
184
|
+
* bidirectional mappings between domain models, storage tables, field names, and column names.
|
|
185
|
+
*
|
|
186
|
+
* @param contractJson The parsed Prisma 8 contract JSON object.
|
|
187
|
+
* @returns A ContractMetadata object with indexed model and column lookups.
|
|
188
|
+
*/
|
|
189
|
+
declare function extractContractMetadata(contractJson: any): ContractMetadata;
|
|
143
190
|
/**
|
|
144
191
|
* Inspects a compiled Prisma ORM 8 `contract.json` object and extracts a
|
|
145
192
|
* dictionary of normalized table names mapped to their column list.
|
|
146
193
|
*
|
|
147
194
|
* @param contractJson The parsed Prisma 8 contract JSON object.
|
|
148
195
|
* @returns A mapping from normalized table names to array of column names.
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```typescript
|
|
152
|
-
* import contractJson from "./contract.json" with { type: "json" };
|
|
153
|
-
*
|
|
154
|
-
* const columns = extractColumns(contractJson);
|
|
155
|
-
* console.log(columns["user"]); // ["id", "email", "name", ...]
|
|
156
|
-
* ```
|
|
157
196
|
*/
|
|
158
197
|
declare function extractColumns(contractJson: any): Record<string, string[]>;
|
|
159
198
|
/**
|
|
160
199
|
* Factory creating Prisma ORM 8 SQL middleware for automatic runtime Zod validation.
|
|
161
200
|
*
|
|
162
201
|
* Hooks into the query compiler via `beforeCompile` and intercepts `insert` and `update`
|
|
163
|
-
* operations.
|
|
164
|
-
*
|
|
202
|
+
* operations. Resolves domain model names to physical storage tables, maps model field
|
|
203
|
+
* names to database columns, validates values, and writes back coerced, transformed,
|
|
204
|
+
* and defaulted values into the AST parameter references.
|
|
165
205
|
*
|
|
166
206
|
* @param options.contract The parsed Prisma 8 `contract.json` object.
|
|
167
|
-
* @param options.validators An array of validator entries
|
|
207
|
+
* @param options.validators An array of validator entries or schemas created by createZodRegistry / createInsertSchema.
|
|
168
208
|
* @returns An `AutoZodMiddleware` instance ready to be passed to `postgres({ middleware: [...] })`.
|
|
169
209
|
*
|
|
170
210
|
* @example
|
|
@@ -192,12 +232,69 @@ declare function extractColumns(contractJson: any): Record<string, string[]>;
|
|
|
192
232
|
*/
|
|
193
233
|
declare function autoZodPrisma8(options: {
|
|
194
234
|
contract: any;
|
|
195
|
-
validators: ModelValidatorEntry
|
|
235
|
+
validators: (ModelValidatorEntry | (z.ZodObject<any> & {
|
|
236
|
+
tableName?: string;
|
|
237
|
+
}))[];
|
|
196
238
|
}): AutoZodMiddleware;
|
|
197
239
|
/**
|
|
198
240
|
* Convenient alias for `autoZodPrisma8`.
|
|
199
241
|
*/
|
|
200
242
|
declare const autoZod: typeof autoZodPrisma8;
|
|
201
243
|
|
|
202
|
-
|
|
203
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Refinement mapping allowing custom Zod schemas or mutator functions per field.
|
|
246
|
+
*/
|
|
247
|
+
type SchemaRefinements = Record<string, z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny)>;
|
|
248
|
+
/**
|
|
249
|
+
* Map Prisma 8 SQL codec IDs to their corresponding base Zod types.
|
|
250
|
+
*/
|
|
251
|
+
declare function codecToZodSchema(codecId?: string): z.ZodTypeAny;
|
|
252
|
+
/**
|
|
253
|
+
* Automatically infers a complete Zod insert schema from a Prisma ORM 8 `contract.json`.
|
|
254
|
+
*
|
|
255
|
+
* Fields with database defaults (e.g. autoincrement ID, now() timestamps) are automatically
|
|
256
|
+
* marked optional. Nullable columns are marked nullable. You can refine or override individual
|
|
257
|
+
* fields with custom validation rules.
|
|
258
|
+
*
|
|
259
|
+
* @param contract The parsed Prisma 8 `contract.json` object.
|
|
260
|
+
* @param modelName The model name in your contract (e.g., "User" or "Post").
|
|
261
|
+
* @param refinements Optional mapping to customize or extend specific field schemas.
|
|
262
|
+
* @returns A ZodObject schema with a `.tableName` property ready for route validation or AutoZod middleware.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* import contractJson from "./contract.json" with { type: "json" };
|
|
267
|
+
* import { createInsertSchema } from "prisma-extension-autozod";
|
|
268
|
+
*
|
|
269
|
+
* export const userCreateSchema = createInsertSchema(contractJson, "User", {
|
|
270
|
+
* email: (s) => s.email("Must be a valid email"),
|
|
271
|
+
* name: (s) => s.min(2),
|
|
272
|
+
* });
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
declare function createInsertSchema<TContract extends Record<string, any> = any, TModel extends string = string>(contract: TContract, modelName: TModel, refinements?: SchemaRefinements): z.ZodObject<any> & {
|
|
276
|
+
tableName: TModel;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Automatically infers a Zod update schema from a Prisma ORM 8 `contract.json`.
|
|
280
|
+
*
|
|
281
|
+
* Equivalent to `createInsertSchema(...).partial()`.
|
|
282
|
+
*
|
|
283
|
+
* @param contract The parsed Prisma 8 `contract.json` object.
|
|
284
|
+
* @param modelName The model name in your contract.
|
|
285
|
+
* @param refinements Optional mapping to customize or extend specific field schemas.
|
|
286
|
+
* @returns A partial ZodObject schema with a `.tableName` property.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```typescript
|
|
290
|
+
* export const userUpdateSchema = createUpdateSchema(contractJson, "User", {
|
|
291
|
+
* email: (s) => s.email(),
|
|
292
|
+
* });
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
declare function createUpdateSchema<TContract extends Record<string, any> = any, TModel extends string = string>(contract: TContract, modelName: TModel, refinements?: SchemaRefinements): z.ZodObject<any> & {
|
|
296
|
+
tableName: TModel;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export { autoZod, autoZodPrisma8, codecToZodSchema, createInsertSchema, createStrictZodRegistry, createUpdateSchema, createZodRegistry, extractColumns, extractContractMetadata, extractValue, isDefaultValueExpr, normalizeKey };
|
|
300
|
+
export type { AutoZodMiddleware, CleanKey, ContractMetadata, InvalidContractKeys, MissingContractKeys, ModelContractMeta, ModelFieldMeta, ModelValidatorEntry, ResolveColumn, SchemaRefinements, ValidateZodAgainstContract };
|
package/dist/index.js
CHANGED
|
@@ -2,68 +2,200 @@
|
|
|
2
2
|
function normalizeKey(key) {
|
|
3
3
|
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4
4
|
}
|
|
5
|
+
function isDefaultValueExpr(val) {
|
|
6
|
+
if (!val || typeof val !== "object") return false;
|
|
7
|
+
return val.kind === "default-value" || val.constructor?.name === "DefaultValueExpr";
|
|
8
|
+
}
|
|
5
9
|
function extractValue(val) {
|
|
10
|
+
if (isDefaultValueExpr(val)) {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
6
13
|
if (val && typeof val === "object" && "value" in val) {
|
|
7
14
|
return val.value;
|
|
8
15
|
}
|
|
9
16
|
return val;
|
|
10
17
|
}
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
18
|
+
function extractContractMetadata(contractJson) {
|
|
19
|
+
const modelsByName = /* @__PURE__ */ new Map();
|
|
20
|
+
const modelsByTable = /* @__PURE__ */ new Map();
|
|
21
|
+
const columnsByTable = {};
|
|
22
|
+
const domainNamespaces = contractJson?.domain?.namespaces || {};
|
|
23
|
+
const storageNamespaces = contractJson?.storage?.namespaces || {};
|
|
24
|
+
const storageDefaults = {};
|
|
25
|
+
for (const [, ns] of Object.entries(storageNamespaces)) {
|
|
15
26
|
const tables = ns?.entries?.table || ns?.tables || {};
|
|
16
|
-
for (const [
|
|
17
|
-
|
|
27
|
+
for (const [tableName, tableDef] of Object.entries(tables)) {
|
|
28
|
+
const cleanTable = normalizeKey(tableName);
|
|
29
|
+
const cols = Object.keys(tableDef?.columns || {});
|
|
30
|
+
columnsByTable[cleanTable] = cols;
|
|
31
|
+
storageDefaults[cleanTable] = {};
|
|
32
|
+
for (const [colName, colDef] of Object.entries(
|
|
33
|
+
tableDef?.columns || {}
|
|
34
|
+
)) {
|
|
35
|
+
storageDefaults[cleanTable][normalizeKey(colName)] = colDef?.default !== void 0;
|
|
36
|
+
}
|
|
18
37
|
}
|
|
19
38
|
}
|
|
20
|
-
|
|
39
|
+
for (const [nsName, ns] of Object.entries(domainNamespaces)) {
|
|
40
|
+
const models = ns?.models || {};
|
|
41
|
+
for (const [modelName, modelDef] of Object.entries(models)) {
|
|
42
|
+
const storageTable = modelDef?.storage?.table || modelName;
|
|
43
|
+
const storageNs = modelDef?.storage?.namespaceId || nsName;
|
|
44
|
+
const cleanModelName = normalizeKey(modelName);
|
|
45
|
+
const cleanStorageTable = normalizeKey(storageTable);
|
|
46
|
+
const modelFieldToColumn = /* @__PURE__ */ new Map();
|
|
47
|
+
const columnToModelField = /* @__PURE__ */ new Map();
|
|
48
|
+
const fields = {};
|
|
49
|
+
const domainFields = modelDef?.fields || {};
|
|
50
|
+
const storageFields = modelDef?.storage?.fields || {};
|
|
51
|
+
for (const [fieldName, fieldDef] of Object.entries(domainFields)) {
|
|
52
|
+
const storageCol = storageFields[fieldName]?.column || fieldName;
|
|
53
|
+
const cleanFieldName = normalizeKey(fieldName);
|
|
54
|
+
const cleanColName = normalizeKey(storageCol);
|
|
55
|
+
modelFieldToColumn.set(cleanFieldName, storageCol);
|
|
56
|
+
modelFieldToColumn.set(cleanColName, storageCol);
|
|
57
|
+
columnToModelField.set(cleanColName, fieldName);
|
|
58
|
+
columnToModelField.set(cleanFieldName, fieldName);
|
|
59
|
+
const hasDefault = Boolean(
|
|
60
|
+
storageDefaults[cleanStorageTable]?.[cleanColName]
|
|
61
|
+
);
|
|
62
|
+
fields[fieldName] = {
|
|
63
|
+
modelFieldName: fieldName,
|
|
64
|
+
storageColumn: storageCol,
|
|
65
|
+
nullable: Boolean(fieldDef?.nullable),
|
|
66
|
+
codecId: fieldDef?.type?.codecId,
|
|
67
|
+
hasDefault
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const meta = {
|
|
71
|
+
modelName,
|
|
72
|
+
storageTable,
|
|
73
|
+
storageNamespace: storageNs,
|
|
74
|
+
modelFieldToColumn,
|
|
75
|
+
columnToModelField,
|
|
76
|
+
fields
|
|
77
|
+
};
|
|
78
|
+
modelsByName.set(cleanModelName, meta);
|
|
79
|
+
modelsByTable.set(cleanStorageTable, meta);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { modelsByName, modelsByTable, columnsByTable };
|
|
83
|
+
}
|
|
84
|
+
function extractColumns(contractJson) {
|
|
85
|
+
const metadata = extractContractMetadata(contractJson);
|
|
86
|
+
return metadata.columnsByTable;
|
|
21
87
|
}
|
|
22
88
|
function autoZodPrisma8(options) {
|
|
23
|
-
const
|
|
89
|
+
const contractMeta = extractContractMetadata(options.contract);
|
|
24
90
|
const validatorMap = /* @__PURE__ */ new Map();
|
|
25
91
|
for (const entry of options.validators) {
|
|
26
|
-
|
|
92
|
+
const rawName = entry.tableName || entry._tableName || entry.name;
|
|
93
|
+
const schema = entry.schema || entry;
|
|
94
|
+
if (rawName && schema) {
|
|
95
|
+
const cleanName = normalizeKey(rawName);
|
|
96
|
+
validatorMap.set(cleanName, schema);
|
|
97
|
+
const meta = contractMeta.modelsByName.get(cleanName) || contractMeta.modelsByTable.get(cleanName);
|
|
98
|
+
if (meta) {
|
|
99
|
+
validatorMap.set(normalizeKey(meta.modelName), schema);
|
|
100
|
+
validatorMap.set(normalizeKey(meta.storageTable), schema);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
27
103
|
}
|
|
28
104
|
return {
|
|
29
105
|
name: "autozod-validator",
|
|
30
106
|
familyId: "sql",
|
|
31
107
|
async beforeCompile(draft, ctx) {
|
|
32
108
|
const ast = draft?.ast;
|
|
33
|
-
if (!ast || ast.kind !== "insert" && ast.kind !== "update")
|
|
34
|
-
|
|
35
|
-
if (!target) return void 0;
|
|
36
|
-
const cleanTarget = normalizeKey(target);
|
|
37
|
-
const schema = validatorMap.get(cleanTarget);
|
|
38
|
-
if (!schema) return void 0;
|
|
39
|
-
const targetCols = columnCatalog[cleanTarget] || [];
|
|
40
|
-
const colLookup = /* @__PURE__ */ new Map();
|
|
41
|
-
for (const col of targetCols) {
|
|
42
|
-
colLookup.set(normalizeKey(col), col);
|
|
109
|
+
if (!ast || ast.kind !== "insert" && ast.kind !== "update") {
|
|
110
|
+
return void 0;
|
|
43
111
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
112
|
+
const rawTable = ast.table?.name;
|
|
113
|
+
if (!rawTable) return void 0;
|
|
114
|
+
const cleanTable = normalizeKey(rawTable);
|
|
115
|
+
const meta = contractMeta.modelsByTable.get(cleanTable) || contractMeta.modelsByName.get(cleanTable);
|
|
116
|
+
const schema = validatorMap.get(cleanTable) || (meta ? validatorMap.get(normalizeKey(meta.modelName)) : void 0);
|
|
117
|
+
if (!schema) return void 0;
|
|
118
|
+
const schemaShape = schema.shape || {};
|
|
119
|
+
const schemaKeys = Object.keys(schemaShape);
|
|
120
|
+
const validateAndWriteBack = (rawRecord, isUpdate) => {
|
|
121
|
+
const toValidate = {};
|
|
122
|
+
const defaultExprFields = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const expectedKey of schemaKeys) {
|
|
124
|
+
if (expectedKey in rawRecord) {
|
|
125
|
+
const raw = rawRecord[expectedKey];
|
|
126
|
+
if (isDefaultValueExpr(raw)) {
|
|
127
|
+
defaultExprFields.add(expectedKey);
|
|
128
|
+
} else {
|
|
129
|
+
toValidate[expectedKey] = extractValue(raw);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const storageCol = meta?.modelFieldToColumn.get(normalizeKey(expectedKey));
|
|
134
|
+
if (storageCol && storageCol in rawRecord) {
|
|
135
|
+
const raw = rawRecord[storageCol];
|
|
136
|
+
if (isDefaultValueExpr(raw)) {
|
|
137
|
+
defaultExprFields.add(expectedKey);
|
|
138
|
+
} else {
|
|
139
|
+
toValidate[expectedKey] = extractValue(raw);
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
46
144
|
for (const [col, val] of Object.entries(rawRecord)) {
|
|
47
|
-
const
|
|
48
|
-
|
|
145
|
+
const mappedModelField = meta?.columnToModelField.get(
|
|
146
|
+
normalizeKey(col)
|
|
147
|
+
);
|
|
148
|
+
if (mappedModelField && mappedModelField in schemaShape) {
|
|
149
|
+
if (isDefaultValueExpr(val)) {
|
|
150
|
+
defaultExprFields.add(mappedModelField);
|
|
151
|
+
} else if (!(mappedModelField in toValidate)) {
|
|
152
|
+
toValidate[mappedModelField] = extractValue(val);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
49
155
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
156
|
+
let activeSchema = isUpdate ? schema.partial() : schema;
|
|
157
|
+
if (!isUpdate && defaultExprFields.size > 0 && typeof schema.partial === "function") {
|
|
158
|
+
const mask = {};
|
|
159
|
+
for (const f of defaultExprFields) {
|
|
160
|
+
mask[f] = true;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
activeSchema = schema.partial(mask);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const parsed = activeSchema.parse(toValidate);
|
|
168
|
+
if (parsed && typeof parsed === "object") {
|
|
169
|
+
for (const [parsedKey, parsedVal] of Object.entries(parsed)) {
|
|
170
|
+
let storageCol = meta?.modelFieldToColumn.get(
|
|
171
|
+
normalizeKey(parsedKey)
|
|
172
|
+
);
|
|
173
|
+
if (!storageCol || !(storageCol in rawRecord)) {
|
|
174
|
+
if (parsedKey in rawRecord) {
|
|
175
|
+
storageCol = parsedKey;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (storageCol && storageCol in rawRecord) {
|
|
179
|
+
const current = rawRecord[storageCol];
|
|
180
|
+
if (current && typeof current === "object" && "value" in current) {
|
|
181
|
+
current.value = parsedVal;
|
|
182
|
+
} else if (isDefaultValueExpr(current)) {
|
|
183
|
+
rawRecord[storageCol] = { value: parsedVal };
|
|
184
|
+
} else {
|
|
185
|
+
rawRecord[storageCol] = parsedVal;
|
|
186
|
+
}
|
|
187
|
+
} else if (storageCol && !isUpdate) {
|
|
188
|
+
rawRecord[storageCol] = { value: parsedVal };
|
|
189
|
+
}
|
|
56
190
|
}
|
|
57
191
|
}
|
|
58
|
-
const activeSchema = isUpdate ? schema.partial() : schema;
|
|
59
|
-
activeSchema.parse(toValidate);
|
|
60
192
|
};
|
|
61
193
|
if (ast.kind === "insert" && Array.isArray(ast.rows)) {
|
|
62
194
|
for (const row of ast.rows) {
|
|
63
|
-
|
|
195
|
+
validateAndWriteBack(row, false);
|
|
64
196
|
}
|
|
65
197
|
} else if (ast.kind === "update" && ast.set) {
|
|
66
|
-
|
|
198
|
+
validateAndWriteBack(ast.set, true);
|
|
67
199
|
}
|
|
68
200
|
return void 0;
|
|
69
201
|
}
|
|
@@ -88,11 +220,86 @@ function createStrictZodRegistry() {
|
|
|
88
220
|
};
|
|
89
221
|
};
|
|
90
222
|
}
|
|
223
|
+
|
|
224
|
+
// src/schema-generator.ts
|
|
225
|
+
import { z } from "zod";
|
|
226
|
+
function codecToZodSchema(codecId) {
|
|
227
|
+
if (!codecId) return z.any();
|
|
228
|
+
const id = codecId.toLowerCase();
|
|
229
|
+
if (id.includes("uuid")) {
|
|
230
|
+
return z.string().uuid();
|
|
231
|
+
}
|
|
232
|
+
if (id.includes("/text") || id.includes("/varchar") || id.includes("/char") || id.includes("/citext")) {
|
|
233
|
+
return z.string();
|
|
234
|
+
}
|
|
235
|
+
if (id.includes("/int2") || id.includes("/int4")) {
|
|
236
|
+
return z.number().int();
|
|
237
|
+
}
|
|
238
|
+
if (id.includes("/float") || id.includes("/numeric")) {
|
|
239
|
+
return z.number();
|
|
240
|
+
}
|
|
241
|
+
if (id.includes("/int8")) {
|
|
242
|
+
return z.union([z.bigint(), z.number()]);
|
|
243
|
+
}
|
|
244
|
+
if (id.includes("/bool")) {
|
|
245
|
+
return z.boolean();
|
|
246
|
+
}
|
|
247
|
+
if (id.includes("/time") || id.includes("/date")) {
|
|
248
|
+
return z.union([z.date(), z.string()]);
|
|
249
|
+
}
|
|
250
|
+
if (id.includes("/json")) {
|
|
251
|
+
return z.any();
|
|
252
|
+
}
|
|
253
|
+
if (id.includes("/bytea")) {
|
|
254
|
+
return z.union([z.instanceof(Uint8Array), z.string()]);
|
|
255
|
+
}
|
|
256
|
+
return z.any();
|
|
257
|
+
}
|
|
258
|
+
function createInsertSchema(contract, modelName, refinements) {
|
|
259
|
+
const metadata = extractContractMetadata(contract);
|
|
260
|
+
const modelMeta = metadata.modelsByName.get(normalizeKey(modelName)) || metadata.modelsByTable.get(normalizeKey(modelName));
|
|
261
|
+
if (!modelMeta) {
|
|
262
|
+
const available = Array.from(metadata.modelsByName.values()).map((m) => m.modelName).join(", ");
|
|
263
|
+
throw new Error(
|
|
264
|
+
`[prisma-extension-autozod]: Model '${modelName}' not found in contract. Available models: ${available || "none"}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const shape = {};
|
|
268
|
+
for (const [fieldName, fieldMeta] of Object.entries(modelMeta.fields)) {
|
|
269
|
+
let fieldSchema = codecToZodSchema(fieldMeta.codecId);
|
|
270
|
+
if (fieldMeta.nullable) {
|
|
271
|
+
fieldSchema = fieldSchema.nullable();
|
|
272
|
+
}
|
|
273
|
+
if (fieldMeta.hasDefault) {
|
|
274
|
+
fieldSchema = fieldSchema.optional();
|
|
275
|
+
}
|
|
276
|
+
const custom = refinements?.[fieldName] || refinements?.[fieldMeta.storageColumn];
|
|
277
|
+
if (custom) {
|
|
278
|
+
fieldSchema = typeof custom === "function" ? custom(fieldSchema) : custom;
|
|
279
|
+
}
|
|
280
|
+
shape[fieldName] = fieldSchema;
|
|
281
|
+
}
|
|
282
|
+
const schema = z.object(shape);
|
|
283
|
+
schema.tableName = modelName;
|
|
284
|
+
return schema;
|
|
285
|
+
}
|
|
286
|
+
function createUpdateSchema(contract, modelName, refinements) {
|
|
287
|
+
const insertSchema = createInsertSchema(contract, modelName, refinements);
|
|
288
|
+
const updateSchema = insertSchema.partial();
|
|
289
|
+
updateSchema.tableName = modelName;
|
|
290
|
+
return updateSchema;
|
|
291
|
+
}
|
|
91
292
|
export {
|
|
92
293
|
autoZod,
|
|
93
294
|
autoZodPrisma8,
|
|
295
|
+
codecToZodSchema,
|
|
296
|
+
createInsertSchema,
|
|
94
297
|
createStrictZodRegistry,
|
|
298
|
+
createUpdateSchema,
|
|
95
299
|
createZodRegistry,
|
|
96
300
|
extractColumns,
|
|
301
|
+
extractContractMetadata,
|
|
302
|
+
extractValue,
|
|
303
|
+
isDefaultValueExpr,
|
|
97
304
|
normalizeKey
|
|
98
305
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prisma-extension-autozod",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Contract-aware compile-time linting and runtime Zod validation for Prisma ORM 8 (@prisma/orm-postgres)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=22.0.0"
|
|
21
21
|
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
28
|
+
},
|
|
22
29
|
"keywords": [
|
|
23
30
|
"prisma",
|
|
24
31
|
"prisma8",
|
|
@@ -56,14 +63,5 @@
|
|
|
56
63
|
"typescript": "5.8.2",
|
|
57
64
|
"vitest": "^5.1.0",
|
|
58
65
|
"zod": "^4.6.7"
|
|
59
|
-
},
|
|
60
|
-
"dependencies": {
|
|
61
|
-
"@prisma/orm-family-sql": "8.0.0-rc.22"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
65
|
-
"test": "vitest run",
|
|
66
|
-
"test:watch": "vitest",
|
|
67
|
-
"typecheck": "tsc --noEmit"
|
|
68
66
|
}
|
|
69
|
-
}
|
|
67
|
+
}
|