From c730aed8d2572e3b3ee8211a166c2c77dc8140b3 Mon Sep 17 00:00:00 2001 From: Tim van Dam Date: Mon, 2 Oct 2023 22:22:22 +0200 Subject: [PATCH] feature(injector): PartialFactory (#487) --- packages/injector/src/injector.ts | 108 +++++++++++++++++++++-- packages/injector/tests/injector.spec.ts | 107 +++++++++++++++++++++- 2 files changed, 209 insertions(+), 6 deletions(-) diff --git a/packages/injector/src/injector.ts b/packages/injector/src/injector.ts index cbdb19c4c..bd1a0018a 100644 --- a/packages/injector/src/injector.ts +++ b/packages/injector/src/injector.ts @@ -10,8 +10,19 @@ import { TagRegistry, Token } from './provider.js'; -import { AbstractClassType, ClassType, CompilerContext, CustomError, getClassName, getPathValue, isArray, isClass, isFunction, isPrototypeOfBase } from '@deepkit/core'; -import { findModuleForConfig, getScope, InjectorModule, PreparedProvider } from './module.js'; +import { + AbstractClassType, + ClassType, + CompilerContext, + CustomError, + getClassName, + getPathValue, + isArray, + isClass, + isFunction, + isPrototypeOfBase +} from '@deepkit/core'; +import {findModuleForConfig, getScope, InjectorModule, PreparedProvider} from './module.js'; import { hasTypeInformation, isExtendable, @@ -111,7 +122,6 @@ function createTransientInjectionTarget(destination: Destination | undefined) { return new TransientInjectionTarget(destination.token); } - let CircularDetector: any[] = []; let CircularDetectorResets: (() => void)[] = []; @@ -225,6 +235,12 @@ export class TransientInjectionTarget { } } +/** + * A factory function for some class. + * All properties that are not provided will be resolved using the injector that was used to create the factory. + */ +export type PartialFactory = (args: Partial<{ [K in keyof C]: C[K] }>) => C; + /** * This is the actual dependency injection container. * Every module has its own injector. @@ -232,7 +248,7 @@ export class TransientInjectionTarget { * @reflection never */ export class Injector implements InjectorInterface { - private resolver?: (token: any, scope?: Scope) => any; + private resolver?: (token: any, scope?: Scope, destination?: Destination) => any; private setter?: (token: any, value: any, scope?: Scope) => any; private instantiations?: (token: any, scope?: string) => number; @@ -612,6 +628,13 @@ export class Injector implements InjectorInterface { return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`; } + if (options.type.kind === ReflectionKind.function && options.type.typeName === 'PartialFactory') { + const type = options.type.typeArguments?.[0]; + const factory = partialFactory(type, this); + const factoryVar = compiler.reserveConst(factory, 'factory'); + return `${factoryVar}(scope)`; + } + if (options.type.kind === ReflectionKind.objectLiteral) { const pickArguments = getPickArguments(options.type); if (pickArguments) { @@ -763,6 +786,12 @@ export class Injector implements InjectorInterface { return new type.classType(args); } + if (type.kind === ReflectionKind.function && type.typeName === 'PartialFactory') { + const factoryType = type.typeArguments?.[0]; + const factory = partialFactory(factoryType, this); + return (scopeIn?: Scope) => factory(scopeIn); + } + if (isWithAnnotations(type)) { if (type.kind === ReflectionKind.objectLiteral) { const pickArguments = getPickArguments(type); @@ -808,7 +837,8 @@ export class Injector implements InjectorInterface { } current = current.indexAccessOrigin.container; } - return () => config; + + if (config) return () => config; } } @@ -973,3 +1003,71 @@ export function injectedFunction any>(fn: T, injecto } return fn; } + +export function partialFactory( + type: Type | undefined, + injector: Injector, +) { + if (!type) throw new Error('Can not create partial factory for undefined type'); + + // must be lazy because creating resolvers for types that are never resolved & unresolvable will throw + function createLazyResolver(type: Type, label?: string): Resolver { + let resolver: Resolver | undefined = undefined; + return (scope?: Scope) => { + if (!resolver) { + resolver = injector.createResolver(type, scope, label); + } + return resolver(scope); + }; + } + + if (type.kind === ReflectionKind.class) { + const classType = type.classType; + const reflectionClass = ReflectionClass.from(classType); + + const args: { name: string; resolve: (scope?: Scope) => ReturnType> }[] = []; + const constructor = reflectionClass.getMethodOrUndefined('constructor'); + if (constructor) { + for (const parameter of constructor.getParameters()) { + args.push({ + name: parameter.name, + resolve: createLazyResolver(parameter.getType() as Type, parameter.name), + }); + } + } + + const properties = new Map ReturnType>>(); + for (const property of reflectionClass.getProperties()) { + const tokenType = getInjectOptions(property.type); + if (!tokenType) continue; + + properties.set(property.getName(), createLazyResolver(tokenType, property.name)); + } + + return (scope?: Scope) => (partial: Partial<{ [K in keyof T]: T[K] }>) => { + const instance = new classType(...(args.map((v) => partial[v.name as keyof T] ?? v.resolve(scope)))); + for (const [property, resolve] of properties.entries()) { + instance[property] ??= partial[property as keyof T] ?? resolve(scope); + } + return instance as T; + }; + } + + if (type.kind === ReflectionKind.objectLiteral) { + const properties = new Map ReturnType>>(); + for (const property of type.types) { + if (property.kind !== ReflectionKind.propertySignature) continue; + properties.set(property.name, createLazyResolver(property, String(property.name))); + } + + return (scope?: Scope) => (partial: Partial<{ [K in keyof T]: T[K] }>) => { + const obj: any = {}; + for (const [property, resolve] of properties.entries()) { + obj[property] = partial[property as keyof T] ?? resolve(scope); + } + return obj as T; + }; + } + + throw new Error(`Can not create partial factory for ${stringifyType(type, { showFullDefinition: false })}`); +} diff --git a/packages/injector/tests/injector.spec.ts b/packages/injector/tests/injector.spec.ts index ffe916faa..966ac6d4b 100644 --- a/packages/injector/tests/injector.spec.ts +++ b/packages/injector/tests/injector.spec.ts @@ -1,5 +1,13 @@ import { expect, test } from '@jest/globals'; -import { CircularDependencyError, DependenciesUnmetError, injectedFunction, Injector, InjectorContext, TransientInjectionTarget } from '../src/injector.js'; +import { + CircularDependencyError, + DependenciesUnmetError, + injectedFunction, + Injector, + InjectorContext, + TransientInjectionTarget, + PartialFactory, +} from '../src/injector.js'; import { InjectorModule } from '../src/module.js'; import { ReflectionClass, ReflectionKind, ReflectionParameter, ReflectionProperty } from '@deepkit/type'; import { Inject } from '../src/types.js'; @@ -855,3 +863,100 @@ test('TransientInjectionTarget', () => { expect(a.b.target.token).toBe(A); } }); + +test('PartialFactory', () => { + { + class A { + constructor (public b: B, public num: number) { + } + } + + class B { + public b = 'b'; + + constructor() { + } + } + + const injector = Injector.from([B]); + const factory = injector.get>(); + + const a = factory({ + num: 5, + }); + + expect(a.b).toBeInstanceOf(B); + expect(a.num).toBe(5); + } + + { + class A { + public num!: Inject; + public b!: Inject; + } + + class B { + public b = 'b'; + } + + const injector = Injector.from([B]); + const factory = injector.get>(); + + const a = factory({ + num: 6, + }); + + expect(a.b).toBeInstanceOf(B); + expect(a.num).toBe(6); + } + + { + class A { + constructor(public factory: PartialFactory) { + } + } + + class B { + constructor(public num: number, public c: C) { + } + } + + class C { + public c = 'c'; + } + + const injector = Injector.from([A, C]); + + const a = injector.get(A); + const b = a.factory({ num: 5 }); + + expect(a.factory).toBeInstanceOf(Function); + expect(b).toBeInstanceOf(B); + expect(b.num).toBe(5); + expect(b.c).toBeInstanceOf(C); + } + + { + class A { + public b: B; + public num: number; + + constructor(public factory: PartialFactory<{ b: B; num: number }>) { + const { b, num } = factory({ num: 5 }); + this.b = b; + this.num = num; + } + } + + class B { + public b = 'b'; + } + + const injector = Injector.from([A, B]); + const a = injector.get(A); + + expect(injector.get(B)).toBeInstanceOf(B); + expect(a.b).toBeInstanceOf(B); + expect(a.num).toBe(5); + } +});