Skip to content

Commit 5ba3b0a

Browse files
committed
feat: mockAll coverage mode and zodmint/storybook adapter (v2.5.0)
- mockAll(schema, options?) — returns full boundary set for any schema. Numbers: min/min+1/max-1/max. Enums: all values. Booleans: [true, false]. Optional/nullable: includes undefined/null. Union: one per branch. Every returned value passes safeParse. Duplicates removed. - zodmint/storybook — new sub-entry with zodArgTypes() and mockArgs(). Maps Zod types to Storybook controls (text/number/range/boolean/select/date/object). Zero @storybook/* dependencies. - 55 new tests (37 coverage + 18 storybook), 534 total passing - README, CHANGELOG, ROADMAP updated
1 parent c859529 commit 5ba3b0a

14 files changed

Lines changed: 1956 additions & 4 deletions

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ All notable changes to zodmint are documented here. This project follows [Keep a
44

55
---
66

7+
## [2.5.0] - 2026-06-03
8+
9+
### Added
10+
- **`mockAll(schema, options?)`** — returns the full boundary set for a schema. For numbers, generates min, min+1, max-1, max (plus 0 if in range). For enums, returns every value. For booleans, always `[true, false]`. For optionals/nullables, includes `undefined`/`null` alongside the inner type's boundary values. For unions, one value per branch. For arrays, empty/1/2-item variants plus length-constrained boundaries. Duplicates are removed. Every returned value passes `schema.safeParse(v).success === true`. Accepts the same options as `mock()` (`seed`, `session`, `generators`); the `mode` option is ignored.
11+
- **`zodmint/storybook`** — new sub-entry with `zodArgTypes(schema)` and `mockArgs(schema, options?)`. `zodArgTypes` maps a Zod object schema to a Storybook `ArgTypes` record — string→text, number→number (range when both min and max are present), boolean→boolean, enum→select, date→date, object/array→object. `z.optional()` and `z.nullable()` are transparently unwrapped. `z.describe()` populates the `description` field. `mockArgs` is a typed wrapper around `mock()` for generating story `args`. Zero runtime dependencies — no `@storybook/*` import required.
12+
13+
---
14+
15+
## [2.4.0] - 2026-05-29
16+
17+
### Added
18+
- **`zodmint/seed`** — new sub-entry for schema-driven database seeding. `seed(inserter, schema, options?)` generates `count` valid fixtures and inserts them via any async function. Returns the full array of generated items for chaining.
19+
- **`prismaInserter(model)`** — wraps a Prisma model delegate (`createMany`) into a `SeedInserter`. Zero runtime dependency: uses duck typing, so no `@prisma/client` import is required in `zodmint/seed` itself.
20+
- **`drizzleInserter(db, table)`** — wraps a Drizzle `db.insert(table).values()` call into a `SeedInserter`. Same approach: duck typed, no drizzle-orm import.
21+
- **`SeedOptions`** — extends `MockOptions` with `count` (default 10), `batchSize` (default: single batch), and `async` (uses `mockAsync()` when true, for schemas with async refinements).
22+
- **Batched inserts** — when `batchSize < count`, records are split into chunks and inserted sequentially, respecting ORM and DB row limits per statement.
23+
- **Seeded determinism** — when a `seed` value is provided, each item receives an offset seed (`seed + i`) so items are distinct but the full result set is reproducible.
24+
- `@prisma/client >= 5.0.0` and `drizzle-orm >= 0.29.0` added as optional peer dependencies.
25+
26+
---
27+
728
## [2.3.0] - 2026-05-29
829

930
### Added

README.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,12 +892,48 @@ Known gaps:
892892

893893
**ORM/OpenAPI ingestion** — Only makes sense after relational generation matures.
894894

895+
**Relational dataset DSL**`dataset()` + `model().belongsTo()` for generating fully coherent multi-schema datasets with referential integrity. Planned for v3.
896+
895897
---
896898

897899
Zod v4 uses `new Function()` internally to compile schema validators. If your environment disables `unsafe-eval` (e.g. via CSP), stick with Zod v3.
898900

899901
---
900902

903+
## Coverage Mode — `mockAll`
904+
905+
`mockAll(schema, options?)` returns the full boundary set for a schema — every interesting edge case — instead of a single value. Use it to systematically exercise constraint boundaries without writing the boundary values yourself.
906+
907+
```typescript
908+
import { mockAll } from "zodmint";
909+
910+
// Numbers: min, min+1, max-1, max (plus 0 if in range)
911+
mockAll(z.number().int().min(18).max(100));
912+
// → [0, 18, 19, 99, 100]
913+
914+
// Enums: every value
915+
mockAll(z.enum(["admin", "user", "guest"]));
916+
// → ["admin", "user", "guest"]
917+
918+
// Booleans: both values
919+
mockAll(z.boolean());
920+
// → [true, false]
921+
922+
// Optional: includes undefined
923+
mockAll(z.string().optional());
924+
// → [undefined, "", "a", "abc"]
925+
926+
// Union: one value per branch
927+
mockAll(z.union([z.string().uuid(), z.number().int().positive()]));
928+
// → ["3f2e1d4c-...", 1]
929+
```
930+
931+
Every value in the returned array is guaranteed to pass `schema.safeParse(v).success === true`. Duplicates are automatically removed.
932+
933+
`mockAll` accepts the same options as `mock()``seed`, `session`, `generators` — and forwards them to any internal generation calls. The `mode` option is ignored: `mockAll` always uses boundary-aware generation.
934+
935+
---
936+
901937
## Integrations
902938

903939
### fast-check
@@ -1014,6 +1050,103 @@ Or add a script to `package.json`:
10141050

10151051
---
10161052

1053+
### Database Seeding — `zodmint/seed`
1054+
1055+
zodmint ships a `zodmint/seed` sub-entry for generating and inserting bulk fixtures directly into your database. It works with any ORM via a plain async inserter function, and ships first-class adapters for Prisma and Drizzle.
1056+
1057+
```bash
1058+
# No extra install needed — seed works with whatever ORM you already have
1059+
```
1060+
1061+
```typescript
1062+
import { seed, prismaInserter, drizzleInserter } from "zodmint/seed";
1063+
```
1064+
1065+
**Prisma:**
1066+
1067+
```typescript
1068+
import { PrismaClient } from "@prisma/client";
1069+
import { seed, prismaInserter } from "zodmint/seed";
1070+
1071+
const prisma = new PrismaClient();
1072+
1073+
const users = await seed(prismaInserter(prisma.user), UserSchema, { count: 50 });
1074+
// users.length === 50, each passes UserSchema.safeParse ✓
1075+
// prisma.user.createMany was called with all 50 records
1076+
```
1077+
1078+
**Drizzle:**
1079+
1080+
```typescript
1081+
import { drizzle } from "drizzle-orm/node-postgres";
1082+
import { seed, drizzleInserter } from "zodmint/seed";
1083+
import { users } from "./schema";
1084+
1085+
const db = drizzle(pool);
1086+
1087+
await seed(drizzleInserter(db, users), UserSchema, { count: 50 });
1088+
```
1089+
1090+
**Plain function — works with any ORM or custom writer:**
1091+
1092+
```typescript
1093+
await seed(
1094+
(data) => prisma.user.createMany({ data }),
1095+
UserSchema,
1096+
{ count: 50 },
1097+
);
1098+
```
1099+
1100+
`seed()` returns the full array of generated items, so you can chain it with `mockRelated()` or assert against results in tests:
1101+
1102+
```typescript
1103+
const seededUsers = await seed(prismaInserter(prisma.user), UserSchema, { count: 10 });
1104+
// seed related posts after
1105+
const seededPosts = await seed(
1106+
prismaInserter(prisma.post),
1107+
PostSchema,
1108+
{ count: 10, overrides: { userId: seededUsers[0].id } },
1109+
);
1110+
```
1111+
1112+
All `MockOptions` are supported — `seed`, `mode`, `overrides`, `generators`, `session`:
1113+
1114+
```typescript
1115+
// Deterministic — same data every time
1116+
await seed(prismaInserter(prisma.user), UserSchema, { count: 50, seed: 1 });
1117+
1118+
// Unique emails via seq() + session
1119+
const session = createSession();
1120+
await seed(prismaInserter(prisma.user), UserSchema, {
1121+
count: 50,
1122+
session,
1123+
generators: { email: () => `user-${seq("email", session)}@example.com` },
1124+
});
1125+
1126+
// Edge-mode values to stress-test DB constraints
1127+
await seed(prismaInserter(prisma.user), UserSchema, { count: 10, mode: "edge" });
1128+
```
1129+
1130+
**Batched inserts** — for large counts or ORMs with row limits per statement:
1131+
1132+
```typescript
1133+
await seed(prismaInserter(prisma.product), ProductSchema, {
1134+
count: 10_000,
1135+
batchSize: 500, // inserts in chunks of 500
1136+
});
1137+
```
1138+
1139+
**Async schemas** — when the schema contains async `z.superRefine()` predicates, pass `async: true`:
1140+
1141+
```typescript
1142+
await seed(prismaInserter(prisma.user), UniqueEmailSchema, {
1143+
count: 20,
1144+
async: true,
1145+
});
1146+
```
1147+
1148+
---
1149+
10171150
### Snapshot Pinning — `mockPin`
10181151

10191152
`mockPin` generates a value and writes it to a JSON fixture file the first time it runs. Subsequent calls read from the file, giving you a stable snapshot that is always valid and always typed.
@@ -1100,6 +1233,65 @@ const [user, product, order] = mockRelatedThree(
11001233

11011234
---
11021235

1236+
### Storybook — `zodmint/storybook`
1237+
1238+
zodmint ships a `zodmint/storybook` sub-entry with two utilities for wiring Zod schemas into Storybook stories. No Storybook runtime dependency required — the package is a zero-dependency utility that produces plain objects Storybook understands.
1239+
1240+
```typescript
1241+
import { zodArgTypes, mockArgs } from "zodmint/storybook";
1242+
```
1243+
1244+
**`zodArgTypes(schema)`** maps a Zod object schema to a Storybook `ArgTypes` map. Each field becomes an argType with an appropriate control based on its Zod type:
1245+
1246+
```typescript
1247+
const ButtonPropsSchema = z.object({
1248+
label: z.string().describe("Button text"),
1249+
disabled: z.boolean().optional(),
1250+
size: z.enum(["sm", "md", "lg"]),
1251+
priority: z.number().min(1).max(10),
1252+
});
1253+
1254+
export default {
1255+
title: "Components/Button",
1256+
argTypes: zodArgTypes(ButtonPropsSchema),
1257+
};
1258+
// label → { control: "text", description: "Button text" }
1259+
// disabled → { control: "boolean" }
1260+
// size → { control: "select", options: ["sm", "md", "lg"] }
1261+
// priority → { control: { type: "range", min: 1, max: 10 } }
1262+
```
1263+
1264+
**`mockArgs(schema, options?)`** generates a single valid mock value to use as story `args`:
1265+
1266+
```typescript
1267+
export const Default = {
1268+
args: mockArgs(ButtonPropsSchema),
1269+
// { label: "Submit", disabled: false, size: "md", priority: 7 }
1270+
};
1271+
1272+
export const SeededStory = {
1273+
args: mockArgs(ButtonPropsSchema, { seed: 42 }),
1274+
// same args every time — stable for snapshot tests
1275+
};
1276+
```
1277+
1278+
Control mapping:
1279+
1280+
| Zod type | Storybook control |
1281+
|---|---|
1282+
| `z.string()` | `"text"` |
1283+
| `z.number()` | `"number"` |
1284+
| `z.number().min(x).max(y)` | `{ type: "range", min: x, max: y }` |
1285+
| `z.boolean()` | `"boolean"` |
1286+
| `z.enum([...])` / `z.nativeEnum(E)` | `"select"` with `options` |
1287+
| `z.date()` | `"date"` |
1288+
| `z.object({...})` / `z.array(...)` | `"object"` |
1289+
| everything else | `"text"` (fallback) |
1290+
1291+
`z.optional()` and `z.nullable()` are transparently unwrapped — the inner type determines the control. `z.describe("...")` populates the `description` field on the argType.
1292+
1293+
---
1294+
11031295
## Prior art
11041296

11051297
zodmint was built with an eye on the existing ecosystem. Three libraries were studied for orientation:

examples/14-seed.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* Database seeding — zodmint/seed
3+
*
4+
* Generates batches of valid fixtures and inserts them via any async inserter.
5+
* First-class adapters for Prisma and Drizzle are included.
6+
*
7+
* Run: npx tsx examples/14-seed.ts
8+
*/
9+
import { z } from "zod";
10+
import { createSession, seq } from "zodmint";
11+
import { seed, prismaInserter, drizzleInserter } from "zodmint/seed";
12+
13+
const UserSchema = z.object({
14+
id: z.string().uuid(),
15+
email: z.string().email(),
16+
name: z.string(),
17+
role: z.enum(["admin", "user", "guest"]),
18+
active: z.boolean(),
19+
});
20+
21+
// ─── Plain function inserter — works with anything ────────────────────────────
22+
23+
const db: unknown[] = [];
24+
25+
const users = await seed(
26+
async (items) => { db.push(...items); },
27+
UserSchema,
28+
{ count: 5, seed: 1 },
29+
);
30+
31+
console.log("Inserted via plain fn:", users.length, "users");
32+
console.log("All valid:", users.every((u) => UserSchema.safeParse(u).success));
33+
34+
// ─── Prisma adapter ───────────────────────────────────────────────────────────
35+
36+
// In a real project: import { PrismaClient } from '@prisma/client'
37+
// const prisma = new PrismaClient()
38+
39+
// Simulated Prisma model delegate (same shape as the real thing)
40+
const prismaUserModel = {
41+
createMany: async (args: { data: unknown[] }) => {
42+
console.log("\nprisma.user.createMany called with", args.data.length, "records");
43+
return { count: args.data.length };
44+
},
45+
};
46+
47+
const prismaUsers = await seed(
48+
prismaInserter(prismaUserModel),
49+
UserSchema,
50+
{ count: 10, seed: 42 },
51+
);
52+
53+
console.log("Prisma seed:", prismaUsers.length, "users");
54+
55+
// ─── Drizzle adapter ──────────────────────────────────────────────────────────
56+
57+
// In a real project:
58+
// import { drizzle } from 'drizzle-orm/node-postgres'
59+
// import { users } from './schema'
60+
// const db = drizzle(pool)
61+
62+
// Simulated Drizzle db (same shape as the real thing)
63+
const drizzleDb = {
64+
insert: (table: unknown) => ({
65+
values: async (data: unknown[]) => {
66+
console.log("\ndrizzle insert into", table, "with", data.length, "rows");
67+
return data;
68+
},
69+
}),
70+
};
71+
72+
const drizzleUsers = await seed(
73+
drizzleInserter(drizzleDb, "users"),
74+
UserSchema,
75+
{ count: 8, seed: 7 },
76+
);
77+
78+
console.log("Drizzle seed:", drizzleUsers.length, "users");
79+
80+
// ─── Batched insert ───────────────────────────────────────────────────────────
81+
82+
let batchCount = 0;
83+
await seed(
84+
async (batch) => {
85+
batchCount++;
86+
console.log(`\nBatch ${batchCount}: ${batch.length} records`);
87+
},
88+
UserSchema,
89+
{ count: 25, batchSize: 10, seed: 100 },
90+
);
91+
// Batches: [10, 10, 5]
92+
93+
// ─── Unique fields via seq() ──────────────────────────────────────────────────
94+
95+
const session = createSession();
96+
97+
const ContactSchema = z.object({
98+
email: z.string().email(),
99+
name: z.string(),
100+
});
101+
102+
const contacts = await seed(
103+
async () => {},
104+
ContactSchema,
105+
{
106+
count: 5,
107+
session,
108+
generators: {
109+
email: () => `user-${seq("email", session)}@example.com`,
110+
},
111+
},
112+
);
113+
114+
console.log("\nUnique emails:");
115+
contacts.forEach((c) => console.log(" ", c.email));
116+
// user-1@example.com, user-2@example.com, ...
117+
118+
// ─── Async schema refinements ─────────────────────────────────────────────────
119+
120+
const EvenAgeSchema = z.object({
121+
name: z.string(),
122+
age: z.number().int().min(18).max(98).superRefine(async (n, ctx) => {
123+
if (n % 2 !== 0) ctx.addIssue({ code: "custom", message: "must be even" });
124+
}),
125+
});
126+
127+
const evenUsers = await seed(async () => {}, EvenAgeSchema, {
128+
count: 3,
129+
async: true, // use mockAsync() internally
130+
});
131+
132+
console.log("\nEven ages:", evenUsers.map((u) => u.age));
133+
// all even numbers

examples/15-coverage.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// 15-coverage.ts — mockAll: boundary-set generation
2+
import { z } from "zod";
3+
import { mockAll } from "zodmint";
4+
5+
const AgeSchema = z.number().int().min(18).max(100);
6+
console.log("ages:", mockAll(AgeSchema));
7+
8+
const RoleSchema = z.enum(["admin", "user", "guest"]);
9+
console.log("roles:", mockAll(RoleSchema));
10+
11+
console.log("booleans:", mockAll(z.boolean()));
12+
13+
const MaybeStr = z.string().optional();
14+
console.log("optional:", mockAll(MaybeStr));
15+
16+
const IdSchema = z.union([z.string().uuid(), z.number().int().positive()]);
17+
console.log("ids:", mockAll(IdSchema));

0 commit comments

Comments
 (0)