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 CHANGED
@@ -4,12 +4,13 @@
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.
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 Normalization** | No | No | Yes (`first_name` <-> `firstName`) |
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. Define Validated Schemas with Contract Linting
66
+ ### 1. Infer Schemas Directly from contract.json (Recommended)
64
67
 
65
- Bind your Zod schemas to your generated Prisma 8 `models` using `createZodRegistry`:
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("Invalid email address"),
79
- username: z
80
- .string()
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 Safety in Action:**
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
- > "User",
99
- > z.object({
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
- ### 2. Register Middleware in Your Prisma 8 Client
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 { userValidator } from "./schemas.ts";
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: [userValidator],
137
+ validators: [userCreateSchema],
128
138
  }),
129
139
  ],
130
140
  });
@@ -132,19 +142,19 @@ export const db = postgres({
132
142
 
133
143
  ---
134
144
 
135
- ### 3. Automatic Validation & Sanitization
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 the database
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; other fields are not required on updates
166
+ name: "Alice Wonder", // Valid; omitted fields are ignored on updates
157
167
  });
158
168
  ```
159
169
 
160
170
  ---
161
171
 
162
- ### 4. Column Normalization in Action
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
- Incoming payloads from frontend forms or REST APIs often use `snake_case`. The middleware automatically remaps them to match your contract columns:
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
- // Incoming frontend payload uses snake_case:
168
- await db.orm.public.User.create({
169
- first_name: "Alice", // Automatically remapped to `firstName` in Postgres
170
- email: "alice@prisma.io",
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
- ### 5. Error Handling DX
203
+ ### 6. Storage Column & Casing Normalization
177
204
 
178
- Because validation throws standard `ZodError` instances, you can easily catch and format errors for HTTP responses in Express, NestJS, Fastify, or Hono:
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
- import { ZodError } from "zod";
182
-
183
- try {
184
- await db.orm.public.User.create(req.body);
185
- } catch (error) {
186
- if (error instanceof ZodError) {
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.contract`: The raw `contract.json` object.
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 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.5",
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
  }