prisma-extension-autozod 1.0.5 → 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 +98 -71
- 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 +1 -4
package/README.md
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
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.
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -19,8 +20,10 @@
|
|
|
19
20
|
| :--- | :--- | :--- | :--- |
|
|
20
21
|
| **Zero Controller Boilerplate** | No (Manual `.parse()`) | No (Manual `.parse()`) | Yes (Auto-intercepted) |
|
|
21
22
|
| **Prisma 8 Native Architecture** | No | No | Yes (`beforeCompile` / Contract) |
|
|
23
|
+
| **Direct Schema Inference** | No | Yes (Build step) | Yes (`createInsertSchema(contract, "User")`) |
|
|
22
24
|
| **Compile-Time Typo Linter** | No | No | Yes (Immediate IDE feedback) |
|
|
23
|
-
| **Casing
|
|
25
|
+
| **Casing & Column Mapping** | No | No | Yes (`first_name` <-> `firstName`) |
|
|
26
|
+
| **AST Value Write-Back** | No | No | Yes (Transforms and defaults reach DB) |
|
|
24
27
|
|
|
25
28
|
---
|
|
26
29
|
|
|
@@ -34,11 +37,11 @@
|
|
|
34
37
|
|
|
35
38
|
## Features
|
|
36
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`).
|
|
37
44
|
- **Compile-Time Contract Linting**: Catches schema mismatches and invalid fields at build-time with clear TypeScript diagnostics.
|
|
38
|
-
- **Native Prisma ORM 8 Lifecycle Hook**: Hooks directly into the `beforeCompile` AST pipeline without monkey-patching.
|
|
39
|
-
- **Automatic Column Normalization**: Remaps input casing (e.g., `first_name` to `firstName`) based on your contract's column catalog.
|
|
40
|
-
- **Context-Aware Validation**: Full validation for `create`/`insert`, and automatic `.partial()` validation for `update` operations so omitted fields do not fail.
|
|
41
|
-
- **Flexible Strictness**: Choose between `createZodRegistry` (allows omitting auto-generated/default fields like `id` or `createdAt`) and `createStrictZodRegistry` (enforces full table column coverage).
|
|
42
45
|
- **Dual ESM & CJS**: Full TypeScript `.d.ts` declarations included out-of-the-box.
|
|
43
46
|
|
|
44
47
|
---
|
|
@@ -60,51 +63,58 @@ yarn add prisma-extension-autozod zod @prisma/orm-postgres
|
|
|
60
63
|
|
|
61
64
|
## Quickstart
|
|
62
65
|
|
|
63
|
-
### 1.
|
|
66
|
+
### 1. Infer Schemas Directly from contract.json (Recommended)
|
|
64
67
|
|
|
65
|
-
|
|
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
|
|
87
|
+
|
|
88
|
+
If you prefer authoring your schemas manually, bind them to your contract types using `createZodRegistry`:
|
|
66
89
|
|
|
67
90
|
```typescript
|
|
68
91
|
import { z } from "zod";
|
|
69
92
|
import { createZodRegistry } from "prisma-extension-autozod";
|
|
70
93
|
import type { models } from "./contract.d.ts";
|
|
71
94
|
|
|
72
|
-
// Create a registry bound to your Prisma 8 contract models
|
|
73
95
|
const defineModel = createZodRegistry<typeof models.public>();
|
|
74
96
|
|
|
75
97
|
export const userValidator = defineModel(
|
|
76
98
|
"User",
|
|
77
99
|
z.object({
|
|
78
|
-
email: z.string().email(
|
|
79
|
-
username: z
|
|
80
|
-
|
|
81
|
-
.min(2, "Username must be at least 2 characters")
|
|
82
|
-
.nullable()
|
|
83
|
-
.optional(),
|
|
84
|
-
name: z
|
|
85
|
-
.string()
|
|
86
|
-
.min(2, "Name must be at least 2 characters")
|
|
87
|
-
.nullable()
|
|
88
|
-
.optional(),
|
|
100
|
+
email: z.string().email(),
|
|
101
|
+
username: z.string().min(2).nullable().optional(),
|
|
102
|
+
name: z.string().min(2).nullable().optional(),
|
|
89
103
|
}),
|
|
90
104
|
);
|
|
91
105
|
```
|
|
92
106
|
|
|
93
|
-
> **Compile-Time
|
|
107
|
+
> **Compile-Time Typo Protection:**
|
|
94
108
|
> If you add a field that does not exist in the contract:
|
|
95
|
-
>
|
|
96
109
|
> ```typescript
|
|
97
|
-
> defineModel(
|
|
98
|
-
>
|
|
99
|
-
>
|
|
100
|
-
> nonExistentColumn: z.string(), // Error: Field 'nonExistentColumn' does not exist in table contract
|
|
101
|
-
> }),
|
|
102
|
-
> );
|
|
110
|
+
> defineModel("User", z.object({
|
|
111
|
+
> nonExistentColumn: z.string(), // Error: Field 'nonExistentColumn' does not exist in table contract
|
|
112
|
+
> }));
|
|
103
113
|
> ```
|
|
104
114
|
|
|
105
115
|
---
|
|
106
116
|
|
|
107
|
-
###
|
|
117
|
+
### 3. Register Middleware in Your Prisma 8 Client
|
|
108
118
|
|
|
109
119
|
Pass the middleware to your `@prisma/orm-postgres` client setup:
|
|
110
120
|
|
|
@@ -114,7 +124,7 @@ import { contractFromJson } from "@prisma/orm-postgres/static";
|
|
|
114
124
|
import { autoZodPrisma8 } from "prisma-extension-autozod";
|
|
115
125
|
|
|
116
126
|
import contractJson from "./contract.json" with { type: "json" };
|
|
117
|
-
import {
|
|
127
|
+
import { userCreateSchema } from "./schemas.ts";
|
|
118
128
|
|
|
119
129
|
export const contract = contractFromJson(contractJson);
|
|
120
130
|
|
|
@@ -124,7 +134,7 @@ export const db = postgres({
|
|
|
124
134
|
middleware: [
|
|
125
135
|
autoZodPrisma8({
|
|
126
136
|
contract: contractJson,
|
|
127
|
-
validators: [
|
|
137
|
+
validators: [userCreateSchema],
|
|
128
138
|
}),
|
|
129
139
|
],
|
|
130
140
|
});
|
|
@@ -132,19 +142,19 @@ export const db = postgres({
|
|
|
132
142
|
|
|
133
143
|
---
|
|
134
144
|
|
|
135
|
-
###
|
|
145
|
+
### 4. Automatic Validation & Transform Write-Back
|
|
136
146
|
|
|
137
147
|
In the new Prisma ORM, models are accessed via `db.orm.<namespace>.<ModelName>`:
|
|
138
148
|
|
|
139
149
|
```typescript
|
|
140
|
-
// Valid: Passes validation and executes
|
|
150
|
+
// Valid: Passes validation, applies transforms, and executes
|
|
141
151
|
await db.orm.public.User.create({
|
|
142
|
-
email: "alice@prisma.io"
|
|
152
|
+
email: " Alice@Prisma.IO ", // Written back as "alice@prisma.io" in database
|
|
143
153
|
username: "alice",
|
|
144
154
|
name: "Alice Smith",
|
|
145
155
|
});
|
|
146
156
|
|
|
147
|
-
// Invalid: Throws standard ZodError before hitting
|
|
157
|
+
// Invalid: Throws standard ZodError before hitting PostgreSQL
|
|
148
158
|
await db.orm.public.User.create({
|
|
149
159
|
email: "not-an-email",
|
|
150
160
|
username: "a", // too short (< 2 characters)
|
|
@@ -153,52 +163,79 @@ await db.orm.public.User.create({
|
|
|
153
163
|
|
|
154
164
|
// Update: Only supplied fields are validated
|
|
155
165
|
await db.orm.public.User.where({ email: "alice@prisma.io" }).update({
|
|
156
|
-
name: "Alice Wonder", // Valid;
|
|
166
|
+
name: "Alice Wonder", // Valid; omitted fields are ignored on updates
|
|
157
167
|
});
|
|
158
168
|
```
|
|
159
169
|
|
|
160
170
|
---
|
|
161
171
|
|
|
162
|
-
###
|
|
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`).
|
|
163
175
|
|
|
164
|
-
|
|
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:
|
|
165
177
|
|
|
166
178
|
```typescript
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
198
|
});
|
|
172
199
|
```
|
|
173
200
|
|
|
174
201
|
---
|
|
175
202
|
|
|
176
|
-
###
|
|
203
|
+
### 6. Storage Column & Casing Normalization
|
|
177
204
|
|
|
178
|
-
|
|
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:
|
|
179
206
|
|
|
180
207
|
```typescript
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Clean flattened validation errors directly back to the client
|
|
188
|
-
return res.status(400).json({ errors: error.flatten().fieldErrors });
|
|
189
|
-
}
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
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",
|
|
213
|
+
});
|
|
192
214
|
```
|
|
193
215
|
|
|
194
216
|
---
|
|
195
217
|
|
|
196
218
|
## API Reference
|
|
197
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
|
+
|
|
198
236
|
### `createZodRegistry<TModels>()`
|
|
199
237
|
|
|
200
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.
|
|
201
|
-
|
|
202
239
|
```typescript
|
|
203
240
|
const defineModel = createZodRegistry<typeof models.public>();
|
|
204
241
|
const userValidator = defineModel("User", zodSchema);
|
|
@@ -207,7 +244,6 @@ const userValidator = defineModel("User", zodSchema);
|
|
|
207
244
|
### `createStrictZodRegistry<TModels>()`
|
|
208
245
|
|
|
209
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.
|
|
210
|
-
|
|
211
247
|
```typescript
|
|
212
248
|
const defineStrict = createStrictZodRegistry<typeof models.public>();
|
|
213
249
|
const userValidator = defineStrict("User", zodSchema);
|
|
@@ -216,17 +252,8 @@ const userValidator = defineStrict("User", zodSchema);
|
|
|
216
252
|
### `autoZodPrisma8(options)` (alias: `autoZod`)
|
|
217
253
|
|
|
218
254
|
Prisma ORM 8 middleware factory:
|
|
219
|
-
|
|
220
|
-
- `options.
|
|
221
|
-
- `options.validators`: Array of validator entries returned by `createZodRegistry` or `createStrictZodRegistry`.
|
|
222
|
-
|
|
223
|
-
### `extractColumns(contractJson)`
|
|
224
|
-
|
|
225
|
-
Helper function that inspects storage namespaces inside `contract.json` and returns a normalized dictionary of table column names.
|
|
226
|
-
|
|
227
|
-
### `normalizeKey(key: string)`
|
|
228
|
-
|
|
229
|
-
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`.
|
|
230
257
|
|
|
231
258
|
---
|
|
232
259
|
|
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",
|
|
@@ -63,8 +63,5 @@
|
|
|
63
63
|
"typescript": "5.8.2",
|
|
64
64
|
"vitest": "^5.1.0",
|
|
65
65
|
"zod": "^4.6.7"
|
|
66
|
-
},
|
|
67
|
-
"dependencies": {
|
|
68
|
-
"@prisma/orm-family-sql": "8.0.0-rc.22"
|
|
69
66
|
}
|
|
70
67
|
}
|