interface-forge
Version:
A TypeScript library for creating strongly typed mock data factories using Faker.js for test data generation
318 lines (284 loc) • 11 kB
text/typescript
// ZodFactory automatically supports both Zod v3 and v4
import { z } from 'zod/v4'; // Or: import { z } from 'zod'; for v3
import { ZodFactory } from 'interface-forge/zod';
// Define a comprehensive user schema with various Zod types
const AddressSchema = z.object({
city: z.string().min(2).max(50),
country: z.string().default('USA'),
state: z.string().length(2).toUpperCase(),
street: z.string().min(5).max(100),
zipCode: z.string().regex(/^\d{5}$/),
});
const UserProfileSchema = z.object({
address: AddressSchema,
age: z.number().int().min(18).max(120),
avatar: z.url().nullable(),
bio: z.string().max(500).nullable(),
email: z.email(),
id: z.uuid(),
isActive: z.boolean(),
metadata: z.object({
createdAt: z.date(),
lastLoginAt: z.date().nullable(),
loginCount: z.number().int().min(0),
tags: z.array(z.string()).default([]),
updatedAt: z.date(),
}),
permissions: z.array(z.string()).min(1),
phoneNumber: z
.string()
.regex(/^\+?[\d\s-()]+$/)
.optional(),
preferences: z.object({
language: z.enum(['en', 'es', 'fr', 'de']),
notifications: z.object({
email: z.boolean(),
push: z.boolean(),
sms: z.boolean(),
}),
privacy: z.enum(['public', 'friends', 'private']),
theme: z.enum(['light', 'dark', 'auto']),
}),
role: z.enum(['admin', 'moderator', 'user']),
settings: z.map(z.string(), z.unknown()),
socialLinks: z.record(z.string(), z.url()).optional(),
username: z
.string()
.min(3)
.max(20)
.regex(/^[a-zA-Z0-9_]+$/),
website: z.url().optional(),
});
// Create a factory instance
const userFactory = new ZodFactory(UserProfileSchema);
// Example 1: Generate a single user
console.log('=== Example 1: Single User ===');
const singleUser = userFactory.build();
console.log('Generated user:', JSON.stringify(singleUser, null, 2));
// Example 2: Generate with overrides
console.log('\n=== Example 2: User with Overrides ===');
const adminUser = userFactory.build({
permissions: ['read', 'write', 'delete', 'manage_users'],
role: 'admin',
username: 'admin_user',
});
console.log('Admin user:', JSON.stringify(adminUser, null, 2));
// Example 3: Generate multiple users
console.log('\n=== Example 3: Batch Generation ===');
const users = userFactory.batch(3);
console.log(`Generated ${users.length} users`);
users.forEach((user, index) => {
console.log(`User ${index + 1}: ${user.username} (${user.role})`);
});
// Example 4: Using partial factory functions for dynamic data
// The factory function only needs to define the fields you want to customize
// All other fields will be auto-generated from the schema
const dynamicUserFactory = new ZodFactory(UserProfileSchema, (factory) => ({
bio: factory.lorem.paragraph(),
email: factory.internet.email(),
metadata: {
createdAt: factory.date.past(),
lastLoginAt: factory.datatype.boolean() ? factory.date.recent() : null,
loginCount: factory.number.int({ max: 1000, min: 0 }),
tags: factory.helpers.multiple(() => factory.lorem.word(), {
count: 3,
}),
updatedAt: factory.date.recent(),
},
// Only customize these specific fields
username: factory.helpers
.slugify(`${factory.person.firstName()}_${factory.person.lastName()}`)
.toLowerCase(),
// All other required fields (id, age, phoneNumber, etc.) are auto-generated
}));
console.log('\n=== Example 4: Dynamic Factory Function ===');
const dynamicUser = dynamicUserFactory.build();
console.log('Dynamic user:', JSON.stringify(dynamicUser, null, 2));
// Example 5: Complex nested schemas
const BlogPostSchema = z.object({
author: UserProfileSchema,
categories: z.array(z.enum(['tech', 'lifestyle', 'business', 'health'])),
comments: z
.array(
z.object({
author: z.object({
email: z.email(),
name: z.string(),
}),
content: z.string().min(1).max(1000),
createdAt: z.date(),
id: z.uuid(),
likes: z.number().int().min(0),
}),
)
.default([]),
content: z.string().min(100),
excerpt: z.string().max(300),
id: z.uuid(),
metadata: z.object({
likes: z.number().int().min(0),
readTime: z.number().int().min(1).describe('Read time in minutes'),
shares: z.number().int().min(0),
views: z.number().int().min(0),
}),
publishedAt: z.date().nullable(),
slug: z.string().regex(/^[a-z0-9-]+$/),
status: z.enum(['draft', 'published', 'archived']),
tags: z.array(z.string()).min(1).max(10),
title: z.string().min(10).max(200),
});
const blogPostFactory = new ZodFactory(BlogPostSchema);
console.log('\n=== Example 5: Complex Nested Schema ===');
const blogPost = blogPostFactory.build({
publishedAt: new Date(),
slug: 'typescript-zod-tutorial',
status: 'published',
title: 'Getting Started with TypeScript and Zod',
});
console.log('Blog post:', JSON.stringify(blogPost, null, 2));
// Example 6: Using generators for custom field generation
// Note: This example shows how to use custom generators with metadata
// The generators option is part of ZodFactoryOptions
// Add metadata to schema fields to use custom generators
const CustomUserSchema = UserProfileSchema.extend({
email: z.email().describe('customEmail'),
id: z.uuid().describe('userId'),
});
const customFactory = new ZodFactory(CustomUserSchema, {
generators: {
customEmail: () => `user_${Date.now()}@example.com`,
userId: () =>
`USR_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
},
});
console.log('\n=== Example 6: Custom Generators ===');
const customUser = customFactory.build();
console.log('Custom user ID:', customUser.id);
console.log('Custom email:', customUser.email);
// Example 7: Working with unions and discriminated unions
const NotificationSchema = z.discriminatedUnion('type', [
z.object({
body: z.string(),
cc: z.array(z.email()).optional(),
subject: z.string(),
to: z.email(),
type: z.literal('email'),
}),
z.object({
message: z.string().max(160),
phoneNumber: z.string().regex(/^\+\d{10,15}$/),
type: z.literal('sms'),
}),
z.object({
body: z.string().max(250),
data: z.record(z.string(), z.unknown()).optional(),
title: z.string().max(100),
type: z.literal('push'),
}),
]);
const NotificationSystemSchema = z.object({
notifications: z.array(NotificationSchema).min(1),
preferences: z.object({
channels: z.array(z.enum(['email', 'sms', 'push'])).min(1),
quietHours: z.object({
enabled: z.boolean(),
end: z.string().regex(/^\d{2}:\d{2}$/),
start: z.string().regex(/^\d{2}:\d{2}$/),
}),
}),
userId: z.uuid(),
});
const notificationFactory = new ZodFactory(NotificationSystemSchema);
console.log('\n=== Example 7: Discriminated Unions ===');
const notificationSystem = notificationFactory.build();
console.log(
'Notification system:',
JSON.stringify(notificationSystem, null, 2),
);
// Example 8: Recursive schemas with depth control
interface Category {
children: Category[];
id: string;
name: string;
parentId: null | string;
slug: string;
}
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
children: z.array(CategorySchema),
id: z.uuid(),
name: z.string().min(2).max(50),
parentId: z.uuid().nullable(),
slug: z.string().regex(/^[a-z0-9-]+$/),
}),
);
const RootCategorySchema = z.object({
categories: z.array(CategorySchema),
});
// Use maxDepth to control recursion
const categoryFactory = new ZodFactory(RootCategorySchema, { maxDepth: 3 });
console.log('\n=== Example 8: Recursive Schemas ===');
const categoryTree = categoryFactory.build();
console.log('Category tree:', JSON.stringify(categoryTree, null, 2));
// Example 9: Working with transformations
const TransformSchema = z.object({
age: z.number().transform((val) => Math.max(0, val)),
email: z.email().transform((val) => val.toLowerCase()),
name: z.string().transform((val) => val.trim().toLowerCase()),
tags: z
.array(z.string())
.transform((arr) => [...new Set(arr)]) // Remove duplicates
.transform((arr) => arr.sort()), // Sort alphabetically
});
// Note: Transformations are applied by Zod during parsing,
// not during generation, but the factory will generate valid input
const transformFactory = new ZodFactory(TransformSchema);
console.log('\n=== Example 9: Schemas with Transformations ===');
const transformedData = transformFactory.build();
console.log('Generated data (before transform):', transformedData);
// Example 10: Using hooks for data enrichment
const hookedUserFactory = new ZodFactory(UserProfileSchema)
.beforeBuild((params) => {
console.log('Before build hook - params:', params);
// For metadata field, we need to provide all required properties
return {
...params,
metadata: {
createdAt: new Date('2024-01-01'),
lastLoginAt: null,
loginCount: 0,
tags: params.metadata?.tags ?? [],
updatedAt: new Date('2024-01-01'),
},
};
})
.afterBuild((user) => {
console.log('After build hook - adding computed fields');
return {
...user,
fullAddress: `${user.address.street}, ${user.address.city}, ${user.address.state} ${user.address.zipCode}`,
isAdmin: user.role === 'admin',
};
});
console.log('\n=== Example 10: Using Hooks ===');
const hookedUser = hookedUserFactory.build();
console.log('User with computed fields:', {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
fullAddress: (hookedUser as any).fullAddress,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
isAdmin: (hookedUser as any).isAdmin,
username: hookedUser.username,
});
// Validate that all generated data passes Zod validation
console.log('\n=== Validation Check ===');
try {
UserProfileSchema.parse(singleUser);
console.log('✓ Single user passes validation');
BlogPostSchema.parse(blogPost);
console.log('✓ Blog post passes validation');
NotificationSystemSchema.parse(notificationSystem);
console.log('✓ Notification system passes validation');
console.log('\nAll generated data is valid according to Zod schemas!');
} catch (error) {
console.error('Validation error:', error);
}