UNPKG

interface-forge

Version:

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

636 lines (616 loc) 25.7 kB
import { Faker } from '@faker-js/faker'; import { LocaleDefinition } from '@faker-js/faker'; import { Randomizer } from '@faker-js/faker'; import { z } from 'zod/v4'; import { ZodObject } from 'zod/v4'; import { ZodType } from 'zod/v4'; declare type AfterBuildHook<T> = (obj: T) => Promise<T> | T; declare type BeforeBuildHook<T> = (params: Partial<T>) => Partial<T> | Promise<Partial<T>>; declare interface CreateManyOptions<T> { adapter?: PersistenceAdapter<T>; } declare interface CreateOptions<T> { adapter?: PersistenceAdapter<T>; } /** * A factory class for generating type-safe mock data by extending Faker.js functionality. * Provides methods for creating single instances, batches, and complex object compositions * with support for circular references through depth control and persistence. * * @template T - The type of objects this factory generates * @template O - The type of factory options */ declare class Factory<T, O extends FactoryOptions = FactoryOptions, F extends FactoryFunction<T> | PartialFactoryFunction<T> = FactoryFunction<T>> extends Faker { #private; readonly options?: { maxDepth: number; } & Omit<O, 'locale' | 'maxDepth' | 'randomizer'>; protected afterBuildHooks: AfterBuildHook<T>[]; protected beforeBuildHooks: BeforeBuildHook<T>[]; protected readonly factory: F; private defaultAdapter?; constructor(factory: F, { locale, randomizer, ...rest }?: Partial<O>); /** * Adds a hook that will be executed after building the instance. * Hooks are executed in the order they were added and can be either synchronous or asynchronous. * This method returns the factory instance for method chaining. * * @param hook Function that receives the built instance and returns the modified instance * @returns The current Factory instance for method chaining */ afterBuild(hook: AfterBuildHook<T>): this; /** * Generates an array of instances using the factory's schema. * Supports both uniform overrides (same for all instances) and individual overrides per instance. * * @param size Number of instances to generate (must be non-negative integer) * @param kwargs Either a single partial object (applied to all) or an array of partials (one per instance) * @returns Array of generated instances * @throws {Error} If size is negative or not an integer */ batch: (size: number, kwargs?: Partial<T> | Partial<T>[]) => T[]; /** * Creates multiple instances asynchronously, allowing use of async factory functions. * This method supports both synchronous and asynchronous factory functions and hooks. * * @param size Number of instances to generate (must be non-negative integer) * @param kwargs Either a single partial object (applied to all) or an array of partials (one per instance) * @returns Promise that resolves to an array of generated instances * @throws {Error} If size is negative or not an integer * * @example * ```typescript * const UserFactory = new Factory<User>(async (factory) => ({ * id: factory.string.uuid(), * email: factory.internet.email(), * apiKey: await generateApiKey() // async operation * })); * * const users = await UserFactory.batchAsync(5); * ``` */ batchAsync(size: number, kwargs?: Partial<T> | Partial<T>[]): Promise<F extends FactoryFunction<T> ? T[] : Partial<T>[]>; /** * Adds a hook that will be executed before building the instance. * Hooks receive the partial parameters (kwargs) and can modify them before the instance is built. * Multiple hooks are executed in the order they were added. * * @param hook Function that receives partial parameters and returns modified parameters * @returns The current Factory instance for method chaining */ beforeBuild(hook: BeforeBuildHook<T>): this; /** * Generates a single instance of type T using the factory's schema. * Properties can be overridden by passing a partial object. * Synchronous hooks are automatically applied if registered. * If async hooks are registered, a ConfigurationError is thrown. * * @param kwargs Properties to override in the generated instance * @param options Factory options including fixture generation * @returns A new instance with factory-generated values merged with any overrides * @throws {ConfigurationError} If async hooks are registered * @throws {FixtureError} If fixture operations fail * @throws {FixtureValidationError} If fixture validation fails */ build: (kwargs?: Partial<T>, options?: Partial<O>) => F extends FactoryFunction<T> ? T : Partial<T>; /** * Builds an instance asynchronously with all registered hooks applied in sequence. * This method supports both synchronous and asynchronous hooks. * Hooks are executed in the order they were registered. * * @param kwargs Optional properties to override in the generated instance * @param options Factory options including fixture generation * @returns A promise that resolves to the built and processed instance * @throws {Error} If any hook throws an error during execution * @throws {FixtureError} If fixture operations fail * @throws {FixtureValidationError} If fixture validation fails */ buildAsync(kwargs?: Partial<T>, options?: Partial<O>): Promise<F extends FactoryFunction<T> ? T : Partial<T>>; /** * Creates a new factory by merging this factory's schema with additional properties. * Composed properties can be static values or other factory instances. * * @template U The composed type (must extend the base type T) * @param composition Object mapping property names to values or factories * @returns A new factory that generates objects with combined properties */ compose<U extends T>(composition: FactoryComposition<U>): Factory<U>; /** * Creates and persists a single instance using the factory's schema. * Uses the configured persistence adapter if available. * * @param kwargs Optional properties to override in the generated instance * @param options Options including an optional persistence adapter * @returns Promise that resolves with the persisted instance * @throws {ConfigurationError} If no persistence adapter is configured * * @example * ```typescript * // With default adapter * const userFactory = new Factory<User>((faker) => ({ * email: faker.internet.email(), * name: faker.person.fullName() * })).withAdapter(mongooseAdapter); * * const user = await userFactory.create({ name: 'John' }); * * // With adapter in options * const user2 = await userFactory.create( * { name: 'Jane' }, * { adapter: prismaAdapter } * ); * ``` */ create(kwargs?: Partial<T>, options?: CreateOptions<T>): Promise<T>; /** * Creates and persists multiple instances in a batch operation. * Uses the configured persistence adapter if available. * * @param size Number of instances to create and persist * @param kwargs Optional overrides for the instances * @param options Options including an optional persistence adapter * @returns Promise that resolves with the persisted instances * @throws {ConfigurationError} If no persistence adapter is configured * * @example * ```typescript * // Create 5 users with default adapter * const users = await userFactory.createMany(5); * * // With individual overrides * const users2 = await userFactory.createMany(3, [ * { role: 'admin' }, * { role: 'user' }, * { role: 'guest' } * ]); * * // With adapter in options * const users3 = await userFactory.createMany( * 10, * undefined, * { adapter: typeormAdapter } * ); * ``` */ createMany(size: number, kwargs?: Partial<T> | Partial<T>[], options?: CreateManyOptions<T>): Promise<T[]>; /** * Creates a new factory that inherits from this factory's schema with modifications. * Unlike compose(), extend() provides access to the factory instance for dynamic property generation. * * @template U The extended type (must extend the base type T) * @param factoryFn Function that returns properties to merge with the base schema * @returns A new factory with inherited and extended properties */ extend<U extends T>(factoryFn: FactoryFunction<U>): Factory<U>; /** * Creates a generator that yields values from an iterable in sequential, cyclic order. * Values must be explicitly requested via the generator's next() method. When all values * have been yielded, the generator starts over from the beginning. * * @template T The type of elements in the iterable * @param iterable An iterable containing values to cycle through (must not be empty) * @returns A generator that yields values in order, restarting after the last element * @throws {Error} If the iterable is empty */ iterate<U>(iterable: Iterable<U>): Generator<U, U, U>; /** * Creates a new factory where all properties are optional (Partial<T>). * This is useful for creating test data where only specific fields need to be set. * * @returns A new factory that produces Partial<T> objects */ partial(): Factory<Partial<T>>; /** * Creates a generator that yields random values from an iterable without consecutive duplicates. * Each value is randomly selected with replacement, but the generator ensures the same value * is never returned twice in a row (unless the iterable contains only one element). * * @template T The type of elements in the iterable * @param iterable An iterable containing values to sample from (must not be empty) * @returns A generator that yields random values without consecutive repetition * @throws {Error} If the iterable is empty */ sample<U>(iterable: Iterable<U>): Generator<U, U, U>; /** * Creates a reference to a function call for lazy evaluation within factory definitions. * The function and its arguments are stored but not executed until the factory builds an object. * Essential for creating relationships between factories without causing infinite recursion. * * @template C The function type * @param handler The function to call during object generation * @param args Arguments to pass to the function when called * @returns A reference that will execute the function during build */ use<R, A extends unknown[]>(handler: (...args: A) => R, ...args: A): R; /** * Sets the default persistence adapter for this factory instance. * * @param adapter The persistence adapter to use as default * @returns The current Factory instance for method chaining * * @example * ```typescript * import { MongooseAdapter } from './adapters/mongoose-adapter'; * * const userFactory = new Factory<User>((faker) => ({ * id: faker.string.uuid(), * email: faker.internet.email(), * name: faker.person.fullName() * })); * * // Set default adapter * const factoryWithDb = userFactory.withAdapter( * new MongooseAdapter(UserModel) * ); * * // Now all create/createMany calls will use this adapter * const user = await factoryWithDb.create(); * const users = await factoryWithDb.createMany(5); * ``` */ withAdapter(adapter: PersistenceAdapter<T>): this; protected buildWithFixture(filePath: string, kwargs: Partial<T> | undefined, _options: FactoryOptions & O): F extends FactoryFunction<T> ? T : Partial<T>; protected buildWithFixtureAsync(filePath: string, kwargs: Partial<T> | undefined, _options: FactoryOptions & O): Promise<F extends FactoryFunction<T> ? T : Partial<T>>; protected calculateSignature(config: Required<FixtureConfiguration>): string; /* Excluded from this release type: createDepthLimitedProxy */ protected getDefaultFixturePath(): string; protected getFixtureConfig(): Required<FixtureConfiguration>; /* Excluded from this release type: isDepthExceeded */ protected parseFixturePath(filePath: string, config: Required<FixtureConfiguration>): { fixturesDir: string; fullPath: string; }; protected readFixture(fullPath: string): FixtureMetadata | null; protected validateFixture(metadata: FixtureMetadata, config: Required<FixtureConfiguration>): void; protected writeFixture(parsedPath: { fixturesDir: string; fullPath: string; }, data: unknown, config: Required<FixtureConfiguration>): void; } declare type FactoryComposition<T> = { [K in keyof T]?: Factory<T[K]> | T[K]; }; declare type FactoryFunction<T> = (factory: Factory<T>, iteration: number, kwargs?: Partial<T>) => FactorySchema<T> | Promise<FactorySchema<T>>; declare interface FactoryOptions { /** * Fixture configuration for caching generated data */ fixtures?: FixtureConfiguration; /** * Enable fixture generation/loading for this build. * - true: Use default fixture path based on call stack * - string: Use as fixture file path */ generateFixture?: boolean | string; locale?: LocaleDefinition | LocaleDefinition[]; maxDepth?: number; randomizer?: Randomizer; } declare type FactorySchema<T> = { [K in keyof T]: Generator<T[K], T[K], T[K]> | Ref<T[K], (...args: unknown[]) => T[K]> | T[K]; }; declare interface FixtureConfiguration { /** * Base directory for storing fixtures. Defaults to process.cwd() */ basePath?: string; /** * Custom directory name for fixtures. Defaults to '__fixtures__' * Set to empty string to store fixtures in the same directory as the file path */ directory?: string; /** * Whether to include the factory function source in signature calculation. Defaults to true */ includeSource?: boolean; /** * Whether to use subdirectories for fixtures. Defaults to true * When true: fixtures are stored in a subdirectory (e.g., /path/__fixtures__/file.json) * When false: fixtures are stored directly in the path (e.g., /path/file.json) */ useSubdirectory?: boolean; /** * Whether to validate factory signature changes. Defaults to true */ validateSignature?: boolean; } declare interface FixtureMetadata { /** * ISO 8601 timestamp when the fixture was created */ createdAt: string; /** * The actual fixture data */ data: unknown; /** * SHA-256 hash of the factory configuration */ signature: string; /** * Fixture version for future compatibility */ version: number; } declare type PartialFactoryFunction<T> = (factory: Factory<T>, iteration: number) => Partial<FactorySchema<T>> | Promise<Partial<FactorySchema<T>>>; /** * Interface for a persistence adapter. * Adapters are responsible for interacting with the underlying data store * (e.g., database, API) to save generated objects. */ declare interface PersistenceAdapter<T, R = T> { /** * Persists a single generated object to the data store. * * @param data The object to persist. * @returns A promise that resolves with the persisted object, * including any properties assigned by the database (e.g., auto-generated IDs). */ create(data: T): Promise<R>; /** * Persists multiple generated objects to the data store in a batch. * * @param data An array of objects to persist. * @returns A promise that resolves with an array of the persisted objects, * including any properties assigned by the database. */ createMany(data: T[]): Promise<R[]>; } /** * Encapsulates a function and its arguments for deferred execution within factories. * This enables lazy evaluation of nested factory calls, preventing infinite recursion * and allowing complex object relationships to be defined declaratively. * * @template T - The return type of the encapsulated function * @template C - The function type that returns T */ declare class Ref<T, C extends (...args: never[]) => T> { private readonly args; private readonly handler; constructor({ args, handler }: { args: Parameters<C>; handler: C; }); callHandler(): T; } /** * A factory class for generating type-safe mock data from Zod schemas. * * ZodFactory extends the base Factory class to provide automatic generation of mock data * that conforms to Zod schema definitions. It supports all major Zod types including * primitives, objects, arrays, unions, enums, and more. * * Note: This class requires the `zod` package to be installed and uses Zod v4 API. * * @example * Basic usage with a simple schema: * ```typescript * import { z } from 'zod/v4'; * import { ZodFactory } from 'interface-forge/zod'; * * const UserSchema = z.object({ * id: z.string().uuid(), * name: z.string().min(1).max(100), * email: z.string().email(), * age: z.number().int().min(18).max(120) * }); * * const factory = new ZodFactory(UserSchema); * const user = factory.build(); * ``` * * @example * Using partial factory functions for custom generation: * ```typescript * const factory = new ZodFactory(UserSchema, (faker) => ({ * name: faker.person.fullName(), * })); * ``` * * @template T - Must be a ZodObject type that defines the schema shape * @template O - Factory options extending ZodFactoryOptions * * @see {@link https://github.com/goldziher/interface-forge/blob/main/examples/07-zod-basic.ts | Basic Example} * @see {@link https://github.com/goldziher/interface-forge/blob/main/examples/07-zod-integration.ts | Advanced Example} * @see {@link https://github.com/goldziher/interface-forge/blob/main/examples/07-zod-testing.ts | Testing Example} */ export declare class ZodFactory<T extends ZodObject<any, any>, O extends ZodFactoryOptions = ZodFactoryOptions> extends Factory<z.output<T>, O> { private readonly generator; private readonly schema; /** * Creates a new ZodFactory instance. * * @param schema - The Zod object schema to generate data from * @param optionsOrFactory - Either factory options or a partial factory function * @param options - Factory options (when second parameter is a factory function) * * @example * Simple schema-based generation: * ```typescript * const factory = new ZodFactory(UserSchema); * ``` * * @example * With partial factory function: * ```typescript * const factory = new ZodFactory(UserSchema, (faker) => ({ * name: faker.person.fullName(), * // Other fields auto-generated * })); * ``` * * @example * With options: * ```typescript * const factory = new ZodFactory(UserSchema, { * maxDepth: 3, * generators: { userId: () => 'custom-id' } * }); * ``` */ constructor(schema: T, optionsOrFactory?: O | PartialFactoryFunction<z.output<T>>, options?: O); /** * Generates a batch of instances that conform to the Zod schema. * * @param size - Number of instances to generate * @param kwargs - Optional overrides for each instance * @returns Array of generated instances */ batch: (size: number, kwargs?: Partial<z.output<T>> | Partial<z.output<T>>[]) => z.output<T>[]; /** * Builds a single instance that conforms to the Zod schema. * * The build process: * 1. Generates data from the Zod schema constraints * 2. Applies any factory function customizations * 3. Applies the provided overrides * 4. Validates the result against the schema * * @param kwargs - Optional property overrides * @param options - Optional build options including fixture generation * @returns A generated instance conforming to the schema * * @example * ```typescript * const user = factory.build({ * role: 'admin', * isActive: true * }); * ``` * * @example * With fixture generation: * ```typescript * const user = factory.build(undefined, { generateFixture: 'user-fixture' }); * ``` */ build: (kwargs?: Partial<z.output<T>>, options?: Partial<O>) => z.output<T>; /** * Register a custom handler for a specific Zod type. * Allows customization of how specific Zod types are generated. * * @param typeName - The Zod type name (e.g., 'ZodCustom', 'ZodBigInt') * @param handler - Function that generates values for this type * @returns The factory instance for method chaining * * @example * const factory = new ZodFactory(schema) * .withTypeHandler('ZodCustom', (schema, generator) => ({ * customField: generator.factory.lorem.word() * })) * .withTypeHandler('ZodBigInt', () => BigInt(42)); */ withTypeHandler(typeName: string, handler: ZodTypeHandler): this; /** * Register multiple type handlers at once. * * @param handlers - Object mapping type names to handler functions * @returns The factory instance for method chaining * * @example * const factory = new ZodFactory(schema).withTypeHandlers({ * ZodCustom: (schema, generator) => ({ id: generator.factory.string.uuid() }), * ZodBigInt: () => BigInt(Math.floor(Math.random() * 1000)) * }); */ withTypeHandlers(handlers: Record<string, ZodTypeHandler>): this; /** * Override buildWithFixture to handle Zod-specific generation * * @param filePath The fixture file path * @param kwargs Optional property overrides * @param _options Build options * @returns The generated Zod object */ protected buildWithFixture(filePath: string, kwargs?: Partial<z.output<T>>, _options?: Partial<O>): z.output<T>; /** * Override signature calculation to include Zod schema information * * @param config Fixture configuration * @returns SHA-256 hash of the factory signature */ protected calculateSignature(config: Required<FixtureConfiguration>): string; } /** * Options for configuring ZodFactory behavior. * */ export declare interface ZodFactoryOptions extends FactoryOptions { /** * Custom generator functions for specific field types. * * Generators are matched by the field's description metadata. * * @example * ```typescript * const factory = new ZodFactory(schema, { * generators: { * // Schema field must have .describe('userId') * userId: () => `USR_${Date.now()}`, * customEmail: () => `test_${Date.now()}@example.com` * } * }); * ``` */ generators?: Record<string, () => unknown>; } /** * Schema generator class that handles recursive generation of values from any ZodType. * This class is responsible for all the complex schema traversal and generation logic. */ declare class ZodSchemaGenerator { readonly factory: ZodFactory<never>; private dateFormatGenerators; private readonly NOT_FOUND; private stringFormatGenerators; private readonly typeHandlers; constructor(factory: ZodFactory<never>); /** * Generate a value from any ZodType schema. * * @param schema * @param currentDepth * * @returns The generated value */ generateFromSchema(schema: ZodType, currentDepth?: number): unknown; getDepthLimitFallback(schema: ZodType): unknown; /** * Register a custom handler for a specific Zod type. * * @param typeName * @param handler * * @returns The factory instance */ registerTypeHandler(typeName: string, handler: ZodTypeHandler): this; /** * Register multiple type handlers at once. * * @param handlers * * @returns The factory instance */ registerTypeHandlers(handlers: Record<string, ZodTypeHandler>): this; private calculateArrayLength; private calculateTargetLength; private extractSizeConstraints; private generateArray; private generateDate; private generateFromRegex; private generateFromZodType; private generateMap; private generateNumber; private generateObject; private generateRecord; private generateSet; private generateString; private generateStringWithConstraints; private generateTuple; private initializeBuiltinHandlers; private tryGenerateFromMapping; private tryGenerateFromMetadata; private tryGenerateFromTypeHandler; } declare type ZodTypeHandler = (schema: ZodType, generator: ZodSchemaGenerator, currentDepth: number) => unknown; export { }