Skip to content

Commit de9af9c

Browse files
feat: add zod schema generator from mongo collection schema
1 parent 1737380 commit de9af9c

3 files changed

Lines changed: 470 additions & 0 deletions

File tree

packages/mizzle-orm/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,12 @@ export type { CollectionDefinition, CollectionMeta } from './types/collection';
4343

4444
export type { OrmContext, OrmConfig, MongoOrm } from './types/orm';
4545

46+
// Validation
47+
export {
48+
generateDocumentSchema,
49+
generateInsertSchema,
50+
generateUpdateSchema,
51+
} from './validation/zod-schema-generator';
52+
4653
// Utilities
4754
export { ObjectId } from 'mongodb';
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/**
2+
* Zod schema generation tests
3+
*/
4+
5+
import { describe, it, expect } from 'vitest';
6+
import { ObjectId } from 'mongodb';
7+
import {
8+
generateDocumentSchema,
9+
generateInsertSchema,
10+
generateUpdateSchema,
11+
} from '../zod-schema-generator';
12+
import { string, number, boolean, date, objectId, publicId, array } from '../../schema/fields';
13+
14+
describe('Zod Schema Generation', () => {
15+
describe('generateDocumentSchema', () => {
16+
it('should generate schema for basic fields', () => {
17+
const schema = {
18+
name: string(),
19+
age: number(),
20+
active: boolean(),
21+
};
22+
23+
const zodSchema = generateDocumentSchema(schema);
24+
25+
// Valid data
26+
const validData = {
27+
name: 'John',
28+
age: 30,
29+
active: true,
30+
};
31+
32+
expect(() => zodSchema.parse(validData)).not.toThrow();
33+
34+
// Invalid data
35+
const invalidData = {
36+
name: 'John',
37+
age: 'thirty', // Should be number
38+
active: true,
39+
};
40+
41+
expect(() => zodSchema.parse(invalidData)).toThrow();
42+
});
43+
44+
it('should respect string validations', () => {
45+
const schema = {
46+
email: string().email().min(5).max(100),
47+
};
48+
49+
const zodSchema = generateDocumentSchema(schema);
50+
51+
// Valid email
52+
expect(() => zodSchema.parse({ email: 'test@example.com' })).not.toThrow();
53+
54+
// Invalid email format
55+
expect(() => zodSchema.parse({ email: 'not-an-email' })).toThrow();
56+
57+
// Too short
58+
expect(() => zodSchema.parse({ email: 'a@b' })).toThrow();
59+
});
60+
61+
it('should respect number validations', () => {
62+
const schema = {
63+
age: number().int().min(0).max(150),
64+
};
65+
66+
const zodSchema = generateDocumentSchema(schema);
67+
68+
// Valid
69+
expect(() => zodSchema.parse({ age: 25 })).not.toThrow();
70+
71+
// Float (should fail for int())
72+
expect(() => zodSchema.parse({ age: 25.5 })).toThrow();
73+
74+
// Negative
75+
expect(() => zodSchema.parse({ age: -5 })).toThrow();
76+
77+
// Too large
78+
expect(() => zodSchema.parse({ age: 200 })).toThrow();
79+
});
80+
81+
it('should handle optional and nullable fields', () => {
82+
const schema = {
83+
optionalField: string().optional(),
84+
nullableField: number().nullable(),
85+
};
86+
87+
const zodSchema = generateDocumentSchema(schema);
88+
89+
// With both fields
90+
expect(() =>
91+
zodSchema.parse({ optionalField: 'hello', nullableField: 42 })
92+
).not.toThrow();
93+
94+
// Without optional
95+
expect(() => zodSchema.parse({ nullableField: 42 })).not.toThrow();
96+
97+
// With null
98+
expect(() =>
99+
zodSchema.parse({ optionalField: 'hello', nullableField: null })
100+
).not.toThrow();
101+
});
102+
103+
it('should handle arrays', () => {
104+
const schema = {
105+
tags: array(string()).min(1).max(5),
106+
};
107+
108+
const zodSchema = generateDocumentSchema(schema);
109+
110+
// Valid
111+
expect(() => zodSchema.parse({ tags: ['tag1', 'tag2'] })).not.toThrow();
112+
113+
// Empty (fails min)
114+
expect(() => zodSchema.parse({ tags: [] })).toThrow();
115+
116+
// Too many (fails max)
117+
expect(() =>
118+
zodSchema.parse({ tags: ['1', '2', '3', '4', '5', '6'] })
119+
).toThrow();
120+
121+
// Wrong item type
122+
expect(() => zodSchema.parse({ tags: [1, 2, 3] })).toThrow();
123+
});
124+
125+
it('should handle ObjectId fields', () => {
126+
const schema = {
127+
userId: objectId(),
128+
};
129+
130+
const zodSchema = generateDocumentSchema(schema);
131+
132+
// Valid ObjectId
133+
expect(() => zodSchema.parse({ userId: new ObjectId() })).not.toThrow();
134+
135+
// Invalid (string)
136+
expect(() => zodSchema.parse({ userId: '123' })).toThrow();
137+
});
138+
139+
it('should handle date fields', () => {
140+
const schema = {
141+
createdAt: date(),
142+
};
143+
144+
const zodSchema = generateDocumentSchema(schema);
145+
146+
// Valid Date
147+
expect(() => zodSchema.parse({ createdAt: new Date() })).not.toThrow();
148+
149+
// Invalid (string)
150+
expect(() => zodSchema.parse({ createdAt: '2024-01-01' })).toThrow();
151+
});
152+
});
153+
154+
describe('generateInsertSchema', () => {
155+
it('should exclude auto-generated fields', () => {
156+
const schema = {
157+
id: publicId('user'),
158+
name: string(),
159+
createdAt: date().defaultNow(),
160+
updatedAt: date().onUpdateNow(),
161+
};
162+
163+
const zodSchema = generateInsertSchema(schema);
164+
165+
// Should only require name (id and timestamps are auto-generated)
166+
const validData = {
167+
name: 'John',
168+
};
169+
170+
expect(() => zodSchema.parse(validData)).not.toThrow();
171+
172+
// Should still allow providing other fields
173+
const dataWithOptional = {
174+
name: 'John',
175+
// id and timestamps would be ignored/overridden anyway
176+
};
177+
178+
expect(() => zodSchema.parse(dataWithOptional)).not.toThrow();
179+
});
180+
181+
it('should make all fields optional', () => {
182+
const schema = {
183+
name: string(),
184+
age: number(),
185+
};
186+
187+
const zodSchema = generateInsertSchema(schema);
188+
189+
// Can provide just name
190+
expect(() => zodSchema.parse({ name: 'John' })).not.toThrow();
191+
192+
// Can provide just age
193+
expect(() => zodSchema.parse({ age: 30 })).not.toThrow();
194+
195+
// Can provide both
196+
expect(() => zodSchema.parse({ name: 'John', age: 30 })).not.toThrow();
197+
198+
// Can provide neither
199+
expect(() => zodSchema.parse({})).not.toThrow();
200+
});
201+
});
202+
203+
describe('generateUpdateSchema', () => {
204+
it('should make all fields optional', () => {
205+
const schema = {
206+
name: string(),
207+
age: number(),
208+
email: string().email(),
209+
};
210+
211+
const zodSchema = generateUpdateSchema(schema);
212+
213+
// Can update just one field
214+
expect(() => zodSchema.parse({ name: 'John' })).not.toThrow();
215+
216+
// Can update multiple fields
217+
expect(() => zodSchema.parse({ name: 'John', age: 30 })).not.toThrow();
218+
219+
// Can update no fields (empty update)
220+
expect(() => zodSchema.parse({})).not.toThrow();
221+
});
222+
223+
it('should exclude auto-managed fields', () => {
224+
const schema = {
225+
id: publicId('user'),
226+
name: string(),
227+
updatedAt: date().onUpdateNow(),
228+
};
229+
230+
const zodSchema = generateUpdateSchema(schema);
231+
232+
// Should allow updating name but not id or updatedAt
233+
const validData = {
234+
name: 'Updated Name',
235+
};
236+
237+
expect(() => zodSchema.parse(validData)).not.toThrow();
238+
});
239+
240+
it('should still validate field types', () => {
241+
const schema = {
242+
age: number(),
243+
email: string().email(),
244+
};
245+
246+
const zodSchema = generateUpdateSchema(schema);
247+
248+
// Valid
249+
expect(() => zodSchema.parse({ age: 30 })).not.toThrow();
250+
251+
// Invalid type
252+
expect(() => zodSchema.parse({ age: 'thirty' })).toThrow();
253+
254+
// Invalid email
255+
expect(() => zodSchema.parse({ email: 'not-an-email' })).toThrow();
256+
});
257+
});
258+
259+
describe('default values', () => {
260+
it('should apply default values', () => {
261+
const schema = {
262+
role: string().default('user'),
263+
active: boolean().default(true),
264+
};
265+
266+
const zodSchema = generateDocumentSchema(schema);
267+
268+
const result = zodSchema.parse({});
269+
270+
expect(result.role).toBe('user');
271+
expect(result.active).toBe(true);
272+
});
273+
});
274+
});

0 commit comments

Comments
 (0)