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 CHANGED
@@ -4,30 +4,44 @@
4
4
  [![CI](https://github.com/abzr0o/prisma-extension-autozod/actions/workflows/ci.yml/badge.svg)](https://github.com/abzr0o/prisma-extension-autozod/actions)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 two layers of safety:
9
+ `prisma-extension-autozod` connects **Zod 4** and **Prisma ORM 8**. It provides three layers of safety:
10
10
 
11
- 1. **Compile-time Linting**: TypeScript flags any field in your Zod schema that does not exist in your Prisma data contract.
12
- 2. **Runtime Middleware**: Automatically intercepts write operations (`create`, `update`, `insert`) via Prisma 8's `beforeCompile` hook, validates payloads, normalizes column naming conventions, and blocks invalid mutations before SQL reaches your database.
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**: `>=4.0.0`
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. Define Validated Schemas with Contract Linting
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
- Bind your Zod schemas to your generated Prisma 8 `models` using `createZodRegistry`:
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("Invalid email address"),
68
- username: z
69
- .string()
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 Safety in Action:**
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
- > "User",
88
- > z.object({
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
- ### 2. Register Middleware in Your Prisma 8 Client
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 { userValidator } from "./schemas.ts";
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: [userValidator],
137
+ validators: [userCreateSchema],
117
138
  }),
118
139
  ],
119
140
  });
@@ -121,19 +142,19 @@ export const db = postgres({
121
142
 
122
143
  ---
123
144
 
124
- ### 3. Automatic Validation & Sanitization
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 the database
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; other fields are not required on updates
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.contract`: The raw `contract.json` object.
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 extractColumns(contractJson) {
43
- const tableCols = {};
44
- const namespaces = contractJson?.storage?.namespaces || {};
45
- for (const ns of Object.values(namespaces)) {
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 [table, def] of Object.entries(tables)) {
48
- tableCols[normalizeKey(table)] = Object.keys(def?.columns || {});
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
- return tableCols;
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 columnCatalog = extractColumns(options.contract);
126
+ const contractMeta = extractContractMetadata(options.contract);
55
127
  const validatorMap = /* @__PURE__ */ new Map();
56
128
  for (const entry of options.validators) {
57
- validatorMap.set(normalizeKey(entry.tableName), entry.schema);
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") return void 0;
65
- const target = ast.table?.name;
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 validateFields = (rawRecord, isUpdate) => {
76
- const extracted = {};
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 actualCol = colLookup.get(normalizeKey(col)) || col;
79
- extracted[actualCol] = extractValue(val);
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
- const schemaShape = schema.shape || {};
82
- const schemaKeys = new Set(Object.keys(schemaShape));
83
- const toValidate = {};
84
- for (const [k, v] of Object.entries(extracted)) {
85
- if (schemaKeys.has(k)) {
86
- toValidate[k] = v;
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
- validateFields(row, false);
232
+ validateAndWriteBack(row, false);
95
233
  }
96
234
  } else if (ast.kind === "update" && ast.set) {
97
- validateFields(ast.set, true);
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 validates AST mutation rows and values.
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. Maps columns according to the contract, validates payload values against
164
- * registered Zod schemas, and throws `ZodError` before SQL execution if invalid.
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 produced by `createZodRegistry`.
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
- export { autoZod, autoZodPrisma8, createStrictZodRegistry, createZodRegistry, extractColumns, normalizeKey };
203
- export type { AutoZodMiddleware, CleanKey, InvalidContractKeys, MissingContractKeys, ModelValidatorEntry, ResolveColumn, ValidateZodAgainstContract };
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 validates AST mutation rows and values.
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. Maps columns according to the contract, validates payload values against
164
- * registered Zod schemas, and throws `ZodError` before SQL execution if invalid.
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 produced by `createZodRegistry`.
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
- export { autoZod, autoZodPrisma8, createStrictZodRegistry, createZodRegistry, extractColumns, normalizeKey };
203
- export type { AutoZodMiddleware, CleanKey, InvalidContractKeys, MissingContractKeys, ModelValidatorEntry, ResolveColumn, ValidateZodAgainstContract };
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 extractColumns(contractJson) {
12
- const tableCols = {};
13
- const namespaces = contractJson?.storage?.namespaces || {};
14
- for (const ns of Object.values(namespaces)) {
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 [table, def] of Object.entries(tables)) {
17
- tableCols[normalizeKey(table)] = Object.keys(def?.columns || {});
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
- return tableCols;
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 columnCatalog = extractColumns(options.contract);
89
+ const contractMeta = extractContractMetadata(options.contract);
24
90
  const validatorMap = /* @__PURE__ */ new Map();
25
91
  for (const entry of options.validators) {
26
- validatorMap.set(normalizeKey(entry.tableName), entry.schema);
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") return void 0;
34
- const target = ast.table?.name;
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 validateFields = (rawRecord, isUpdate) => {
45
- const extracted = {};
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 actualCol = colLookup.get(normalizeKey(col)) || col;
48
- extracted[actualCol] = extractValue(val);
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
- const schemaShape = schema.shape || {};
51
- const schemaKeys = new Set(Object.keys(schemaShape));
52
- const toValidate = {};
53
- for (const [k, v] of Object.entries(extracted)) {
54
- if (schemaKeys.has(k)) {
55
- toValidate[k] = v;
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
- validateFields(row, false);
195
+ validateAndWriteBack(row, false);
64
196
  }
65
197
  } else if (ast.kind === "update" && ast.set) {
66
- validateFields(ast.set, true);
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.4",
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
+ }