feathers-factory
Version:
Create data factories for your Feathers services
1 lines • 18.9 kB
Source Map (JSON)
{"version":3,"sources":["../src/Errors/FeathersFactoryError.ts","../src/TemplateContext.ts","../src/FactoryTemplate.ts","../src/Factory.ts","../src/GlobalFactories.ts"],"sourcesContent":["export default class FeathersFactoryError extends Error {}\nexport class FeathersServiceNotDefined extends FeathersFactoryError {}\n","import Clues from 'clues';\nimport type { Get, Paths } from 'type-fest';\nimport type { FactoryTemplate } from './FactoryTemplate';\n\n/**\n * Factory Template context.\n * Provides access to the current generation context.\n *\n * The `this` type within your template's generator functions.\n */\nexport class TemplateContext<TTemplate, TContext extends TemplateContext<TTemplate> = any> {\n /**\n * Internal state for the getter machine.\n * The structure of this field can be unexpected unless explicitly accessed\n * through the {@link this.get} method.\n * @private\n */\n public readonly _state: ContextState<TTemplate>;\n \n constructor(protected readonly template: FactoryTemplate<TTemplate, TContext>) {\n this._state = Object.create(this);\n \n const entries = Object.entries(this.template._schema).map(([key, value]) => {\n return [key, this.wrapTemplateField(value)]\n })\n \n Object.assign(this._state, Object.fromEntries(entries));\n }\n \n /**\n * Resolve the value of a template field within the current generator\n * context. Fields are only resolved once per generator context.\n *\n * This ensures that you can safely reference the same field multiple times\n * within the same generation context and from different fields.\n *\n * @example\n * template = ({\n * firstName: () => faker.person.firstName(),\n * lastName: () => faker.person.lastName(),\n *\n * fullName: () => `${this.get('firstName')} ${this.get('lastName')}`,\n * // -> John Doe\n *\n * // Functions are only called once, then cached to ensure consistent\n * // results within the same generation context.\n * email: () => `${this.get('firstName')}.${this.get('lastName')}@example.com`\n * // -> John.Doe@example.com,\n * })\n *\n */\n public get<TKey extends Paths<TTemplate> & string>(key: TKey): ContextFieldOutcome<Get<TTemplate, TKey>> {\n return Clues(this._state, key as string, { CONTEXT: this });\n }\n \n /**\n * Run the generator function for a given field. This will not cache the\n * result within the current context. Meaning you can call it multiple times\n * within the same generation context and it will always return a new value.\n *\n * This is useful if you want to extend the result of a field from within\n * another field. Do keep in mind that you might want to use it sparingly\n * in case the field has side-effects. E.g. creating new records in the\n * database.\n *\n * @example\n * template = ({\n * firstName: () => faker.person.firstName(),\n * lastName: () => faker.person.lastName(),\n *\n * fullName: () => `${this.get('firstName')} ${this.get('lastName')}`,\n * // -> John Doe\n *\n * family: () => [\n * this.call('fullName'), // -> <New random name>\n * this.call('fullName'), // -> <New random name>\n *\n * this.get('fullName'), // -> John Doe\n * ]\n */\n public call<TKey extends Paths<TTemplate> & string>(key: TKey): ContextFieldOutcome<Get<TTemplate, TKey>> {\n const freshContext = new TemplateContext(this.template);\n \n return freshContext.get(key);\n }\n \n /**\n * Wrap any template functions around an array to indicate to Clues.js\n * what parameters are expected. Which is just this class instance.\n *\n * Enables use of the context parameter in arrow functions.\n */\n protected wrapTemplateField(field: unknown) {\n if (!this.shouldWrap(field)) {\n return field;\n }\n return ['CONTEXT', function(this: unknown, CONTEXT: TemplateContext<TTemplate>) {\n return field.apply(this, [CONTEXT])\n }]\n }\n \n /**\n * Check whether the provided field is a function we should wrap to help\n * Clues.js resolve input types.\n */\n protected shouldWrap(field: unknown): field is Function {\n if (typeof field !== 'function') {\n return false;\n }\n if (Object.getOwnPropertyDescriptor(field, 'prototype')?.writable === false) {\n return false;\n }\n return true;\n }\n \n /**\n * Attempt to resolve the current context state.\n * Used primarily for testing. The internal state does change during\n * resolve and could yield unexpected results.\n * @private\n */\n public async _resolveState() {\n const result = Object.keys(this._state).map(async (key) => {\n return [key, await this.get(key as any)]\n });\n return Object.fromEntries(await Promise.all(result));\n };\n}\n\n/**\n * Resolver state. May contain some partially resolved template fields.\n */\ntype ContextState<TTemplate> = {\n [key in keyof TTemplate]: ContextField<TTemplate[key]>;\n}\n\n/**\n * Contextualized FactoryTemplate fields.\n * When resolving some fields will get converted to promises,\n * some immediately to their resulting value, etc.\n *\n * Todo: Attempt to infer types that will never get converted to a promise or\n * function. (sync functions, static values, etc.)\n */\ntype ContextField<TType> =\n | TType\n | Promise<TType>\n | (() => Promise<TType> | TType);\n\n/**\n * After resolving a field, it'll either be it's final type or a promise\n * in cases where there's peer dependencies or the field function actually\n * is async.\n */\ntype ContextFieldOutcome<TType> =\n TType extends ContextField<infer T>\n ? Promise<T> | T\n : never;","import { TemplateContext } from './TemplateContext';\n\n/**\n * Factory boilerplate template.\n * Defines the fields that will be generated when factories are called.\n */\nexport class FactoryTemplate<TTemplate, TContext extends TemplateContext<TTemplate> = TemplateContext<TTemplate>> {\n constructor(public readonly _schema: TemplateSchema<TTemplate, TContext>) {}\n \n /**\n * Run all factory functions in the template and return final result to be\n * stored in the database.\n */\n public resolve(overrides?: TemplateOverrides<TTemplate, TContext>): Promise<TemplateResult<TTemplate>> {\n const template = this.extend(overrides || {});\n const context = new TemplateContext(template);\n \n return context._resolveState();\n }\n \n public extend<TOverrides>(overrides: ExtendSchema<TTemplate, TOverrides>): ExtendTemplate<TTemplate, TOverrides> {\n // @ts-expect-error Incompatible types\n return new FactoryTemplate({\n ...this._schema,\n ...overrides,\n })\n }\n}\n\n/**\n * Factory Template definition.\n * Defines the fields that will be generated when the factory is called.\n */\nexport type TemplateSchema<TTemplate, TContext = TemplateContext<TTemplate>> = {\n [key in keyof TTemplate]: TemplateField<TTemplate[key], TContext>;\n} & ThisType<TContext>;\n\n\n/**\n * Factory Template field.\n * Specifies a function to run every time the factory is called. Or a static\n * value that will always remain the same.\n */\ntype TemplateField<\n TValue = unknown,\n TContext = unknown,\n> = TValue | Promise<TValue> | ((context: TContext) => TValue | Promise<TValue>);\n\n/**\n * Factory Template result.\n * The raw output of type of the template after resolving all fields.\n */\nexport type TemplateResult<TTemplate> = {\n [key in keyof TTemplate]: InferFieldType<TTemplate[key]>;\n}\n\n/**\n * Infer the resulting data type of the provided factory template.\n * @example\n * const userTemplate = new FactoryTemplate({\n * userId: () => 123,\n * createdAt: () => new Date()\n * })\n *\n * const data: InferOutput<typeof userTemplate>\n * // -> { userId: number, createdAt: Date }\n */\nexport type InferOutput<TTemplate> = TTemplate extends FactoryTemplate<infer T> ? T : never;\n\n/**\n * Template overrides.\n * Defines the fields that can be overridden before resolving the final\n * template.\n */\nexport type TemplateOverrides<TTemplate, TContext = {}> = {\n [key in keyof TTemplate]?: TemplateField<InferFieldType<TTemplate[key]>, TContext>;\n} & ThisType<TContext>;\n\n/**\n * Infer the resolved output type of a given template field.\n */\nexport type InferFieldType<T> = T extends TemplateField<infer T> ? T : never;\n\n/**\n * Merge two template definitions to create a new template using one as a base.\n */\nexport type ExtendTemplate<\n TTemplate,\n TOverrides,\n TResult = TTemplate & TOverrides,\n TMerged = {\n [key in keyof TResult]: key extends keyof TOverrides\n ? TOverrides[key]\n : TResult[key];\n }\n> = FactoryTemplate<TMerged>;\n\n/**\n * Combine a target schema with a baseline schema.\n */\nexport type ExtendSchema<\n TTemplate,\n TOverrides,\n TResult = TTemplate & TOverrides,\n TMerged = {\n [key in keyof TResult]: key extends keyof TOverrides\n ? TOverrides[key]\n : TResult[key];\n }\n> = TOverrides & ThisType<TemplateContext<TMerged>>;","import { Params } from '@feathersjs/feathers';\nimport { FeathersServiceNotDefined } from './Errors/FeathersFactoryError';\nimport { FactoryTemplate, type TemplateOverrides, type TemplateSchema } from './FactoryTemplate';\nimport type { FactoryCompatibleService } from './ServiceTypes';\n\nexport default class Factory<\n TSchema,\n TResult = TSchema,\n TParams = Params,\n> {\n protected readonly data: FactoryTemplate<TSchema>;\n protected readonly params: FactoryTemplate<Params>;\n \n /**\n * Factory constructor.\n */\n public constructor(\n private readonly service: FactoryCompatibleService<TSchema, TResult, TParams>,\n data: TemplateSchema<TSchema> | FactoryTemplate<TSchema>,\n defaultParams: TemplateSchema<TParams> = {} as any,\n ) {\n if (!service) {\n throw new FeathersServiceNotDefined('The provided service doesn\\'t appear to exist!');\n }\n \n if (data instanceof FactoryTemplate) {\n this.data = data;\n } else {\n this.data = new FactoryTemplate(data);\n }\n \n this.params = new FactoryTemplate(defaultParams);\n }\n \n /**\n * Store generated data to the Feathers service.\n */\n public async create(\n data?: TemplateOverrides<TSchema>,\n params?: TemplateOverrides<TParams>,\n ): Promise<TResult> {\n const resolvedData: any = await this.get(data);\n const resolvedParams = await this.params.resolve(params);\n \n return this.service.create(resolvedData, resolvedParams as TParams) as Promise<TResult>;\n }\n \n /**\n * Quickly populate the database running the factory a number of times.\n */\n public createMany(\n quantity: number,\n overrides?: TemplateOverrides<TSchema>,\n params?: TemplateOverrides<TParams>,\n ): Promise<TResult[]> {\n const promises: Promise<TResult>[] = [];\n \n for (let i = 0; i < quantity; i++) {\n promises.push(this.create(overrides, params));\n }\n \n return Promise.all(promises);\n }\n \n /**\n * Just resolve a predefined factory template without inserting it into\n * the underlying service.\n *\n * @param overrides\n */\n public get(overrides: TemplateOverrides<TSchema> = {}) {\n return this.data.resolve(overrides);\n }\n \n}\n\n","import { Params } from '@feathersjs/feathers';\nimport Factory from './Factory';\nimport type { TemplateOverrides } from './FactoryTemplate';\nimport type { HasBeenAugmented } from './lib/Utilities';\n\n\n/**\n * We are intentionally leaving this empty here so you can add type\n * declarations for globally accessible factories.\n *\n * @example\n * const MyUserFactory = new Factory(...);\n *\n * declare module 'feathers-factory' {\n * interface GlobalFactories {\n * 'user-factory': typeof MyUserFactory\n * }\n * }\n */\nexport interface GlobalFactories {\n\n}\n\n\nexport default new class GlobalFactories {\n\n /**\n * Defined factories.\n */\n public factories = {} as FactoryRegistry;\n\n /**\n * Define a new factory.\n */\n public define<\n TSchema,\n TResult = TSchema,\n >(\n factoryName: FactoryName,\n factory: Factory<TSchema, TResult>,\n ) {\n this.factories[factoryName] = factory;\n }\n \n /**\n * Retrieve a factory name as defined in the define() method.\n *\n * @param name\n */\n public getFactory<TName extends FactoryName>(name: TName): GlobalFactory<TName> {\n const factory = this.factories[name];\n \n if (!factory) {\n throw Error(`Could not locate factory '${name}'. Did you define it?`);\n }\n \n return factory;\n }\n \n\n /**\n * Run factory, creating entry in Feathers service.\n *\n * @param factoryName\n * @param overrides\n * @param params\n */\n public create<\n TName extends FactoryName\n >(factoryName: TName, overrides?: GlobalFactoryOverrides<TName>, params?: Params) {\n const factory = this.getFactory(factoryName);\n\n return factory.create(overrides, params);\n }\n\n /**\n * Run a number of factories, creating entries in Feathers service.\n *\n * @param quantity\n * @param factoryName\n * @param overrides\n * @param params\n */\n public createMany<\n TName extends FactoryName\n >(quantity: number, factoryName: TName, overrides?: TemplateOverrides<any>, params?: Params) {\n const factory = this.getFactory(factoryName);\n return factory.createMany(quantity, overrides, params);\n }\n\n /**\n * Run factory without creating entry in Feathers service.\n * Returns resolved data object.\n *\n * @param factoryName\n * @param overrides\n */\n public get<\n TName extends FactoryName\n >(factoryName: TName, overrides?: TemplateOverrides<TName>) {\n const factory = this.factories[factoryName];\n\n if (!factory) {\n throw Error(`Could not locate factory '${factoryName}'. Did you define it?`);\n }\n\n return factory.get(overrides);\n }\n\n}\n\ntype FactoryName = keyof FactoryRegistry;\ntype GlobalFactory<TName extends FactoryName> = FactoryRegistry[TName];\n\ntype GlobalFactoryOverrides<\n TName extends FactoryName\n> = GlobalFactory<TName> extends Factory<infer TSchema, infer TResult>\n ? TemplateOverrides<TSchema>\n : never;\n\ntype FactoryRegistry = HasBeenAugmented<GlobalFactories> extends true\n ? GlobalFactories\n : DefaultFactoryRegistry\n\ntype DefaultFactoryRegistry = {\n [key: string]: Factory<any, any>;\n}\n\n"],"mappings":"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","names":[]}