UNPKG

interface-forge

Version:

A TypeScript library for creating strongly typed mock data factories using Faker.js for test data generation

312 lines (278 loc) 11 kB
// 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.toSorted()), // 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); }