interface-forge
Version:
A TypeScript library for creating strongly typed mock data factories using Faker.js for test data generation
409 lines (388 loc) • 17.5 kB
TypeScript
import { Faker } from '@faker-js/faker';
import { LocaleDefinition } from '@faker-js/faker';
import { Randomizer } from '@faker-js/faker';
export declare type AfterBuildHook<T> = (obj: T) => Promise<T> | T;
export declare type BeforeBuildHook<T> = (params: Partial<T>) => Partial<T> | Promise<Partial<T>>;
export declare class CircularReferenceError extends Error {
constructor(message?: string);
}
export declare class ConfigurationError extends Error {
constructor(message: string);
}
export declare interface CreateManyOptions<T> {
adapter?: PersistenceAdapter<T>;
}
export 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
*/
export 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;
}
export declare type FactoryComposition<T> = {
[K in keyof T]?: Factory<T[K]> | T[K];
};
export declare type FactoryFunction<T> = (factory: Factory<T>, iteration: number, kwargs?: Partial<T>) => FactorySchema<T> | Promise<FactorySchema<T>>;
export 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;
}
export declare type FactorySchema<T> = {
[K in keyof T]: Generator<T[K], T[K], T[K]> | Ref<T[K], (...args: unknown[]) => T[K]> | T[K];
};
export 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;
}
export declare class FixtureError extends Error {
constructor(message: string);
}
export 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;
}
export declare class FixtureValidationError extends FixtureError {
constructor(message: string);
}
export 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.
*/
export 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
*/
export 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;
}
export declare class ValidationError extends Error {
constructor(message: string);
}
export { }