From 29a8dbb44ef543a8c81f9937a589a9003077de78 Mon Sep 17 00:00:00 2001 From: Tim van Dam Date: Wed, 20 Sep 2023 23:46:05 +0200 Subject: [PATCH] feature(injector): TransientInjectorTarget injectable --- packages/injector/src/injector.ts | 45 +++++++++- packages/injector/tests/injector.spec.ts | 101 ++++++++++++++++++++++- 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/packages/injector/src/injector.ts b/packages/injector/src/injector.ts index 6f5625de4..675044066 100644 --- a/packages/injector/src/injector.ts +++ b/packages/injector/src/injector.ts @@ -96,6 +96,20 @@ function propertyParameterNotFound(ofName: string, name: string, position: numbe ); } +function transientInjectionTargetUnavailable(ofName: string, name: string, position: number, token: any) { + throw new DependenciesUnmetError( + `${TransientInjectionTarget.name} is not available for ${name} of ${ofName}. ${TransientInjectionTarget.name} is only available when injecting ${ofName} into other providers` + ); +} + +function createTransientInjectionTargetForProvider(provider: NormalizedProvider | undefined) { + if (!provider) { + return undefined; + } + + return new TransientInjectionTarget(provider.provide); +} + let CircularDetector: any[] = []; let CircularDetectorResets: (() => void)[] = []; @@ -198,6 +212,18 @@ function getPickArguments(type: Type): Type[] | undefined { return; } +/** + * Class describing where a transient provider will be injected. + * + * @reflection never + */ +export class TransientInjectionTarget { + constructor ( + public readonly token: Token, + ) { + } +} + /** * This is the actual dependency injection container. * Every module has its own injector. @@ -267,6 +293,8 @@ export class Injector implements InjectorInterface { resolverCompiler.context.set('constructorParameterNotFound', constructorParameterNotFound); resolverCompiler.context.set('propertyParameterNotFound', propertyParameterNotFound); resolverCompiler.context.set('factoryDependencyNotFound', factoryDependencyNotFound); + resolverCompiler.context.set('transientInjectionTargetUnavailable', transientInjectionTargetUnavailable); + resolverCompiler.context.set('createTransientInjectionTargetForProvider', createTransientInjectionTargetForProvider); resolverCompiler.context.set('injector', this); const lines: string[] = []; @@ -355,7 +383,7 @@ export class Injector implements InjectorInterface { ${resets.join('\n')}; }); - return function(token, scope) { + return function(token, scope, destinationProvider) { switch (true) { ${lines.join('\n')} } @@ -552,6 +580,16 @@ export class Injector implements InjectorInterface { } } + if (options.type.kind === ReflectionKind.class && options.type.classType === TransientInjectionTarget) { + if (fromProvider.transient === true) { + const tokenVar = compiler.reserveVariable('token', options.type.classType); + const orThrow = options.optional ? '' : `?? transientInjectionTargetUnavailable(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; + return `createTransientInjectionTargetForProvider(destinationProvider) ${orThrow}`; + } else { + throw new Error(`Cannot inject ${TransientInjectionTarget.name} into ${JSON.stringify(ofName)}.${JSON.stringify(options.name)}, as ${JSON.stringify(ofName)} is not transient`); + } + } + if (options.type.kind === ReflectionKind.class && options.type.classType === TagRegistry) { return compiler.reserveVariable('tagRegistry', this.buildContext.tagRegistry); } @@ -683,10 +721,11 @@ export class Injector implements InjectorInterface { const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; + const destinationProvider = compiler.reserveConst(fromProvider); if (resolveFromModule === this.module) { - return `injector.resolver(${tokenVar}, scope)`; + return `injector.resolver(${tokenVar}, scope, ${destinationProvider})`; } - return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope) ${orThrow}`; + return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope, ${destinationProvider}) ${orThrow}`; } createResolver(type: Type, scope?: Scope, label?: string): Resolver { diff --git a/packages/injector/tests/injector.spec.ts b/packages/injector/tests/injector.spec.ts index 7ebf220dc..7925a27de 100644 --- a/packages/injector/tests/injector.spec.ts +++ b/packages/injector/tests/injector.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@jest/globals'; -import { CircularDependencyError, injectedFunction, Injector, InjectorContext } from '../src/injector.js'; +import { CircularDependencyError, DependenciesUnmetError, injectedFunction, Injector, InjectorContext, TransientInjectionTarget } from '../src/injector.js'; import { InjectorModule } from '../src/module.js'; -import { ReflectionClass, ReflectionKind } from '@deepkit/type'; +import { ReflectionClass, ReflectionKind, ReflectionParameter, ReflectionProperty } from '@deepkit/type'; import { Inject } from '../src/types.js'; import { provide } from '../src/provider.js'; @@ -712,3 +712,100 @@ test('injectedFunction skip 2', () => { expect(wrapped(undefined, 'abc', new A)).toBe('abc'); }); + +test('TransientInjectionTarget', () => { + { + class A { + constructor (public readonly b: B) { + } + } + + class B { + constructor ( + public readonly target: TransientInjectionTarget + ) { + } + } + + const injector = Injector.from([A, { provide: B, transient: true }]); + const a = injector.get(A); + expect(a.b.target).toBeInstanceOf(TransientInjectionTarget); + expect(a.b.target.token).toBe(A); + } + + { + class A { + constructor (public readonly b: B) { + } + } + + class B { + constructor ( + public readonly target: TransientInjectionTarget + ) { + } + } + + const injector = Injector.from([ + A, + { provide: B, useFactory: (target: TransientInjectionTarget) => new B(target), transient: true } + ]); + const a = injector.get(A); + expect(a.b.target).toBeInstanceOf(TransientInjectionTarget); + expect(a.b.target.token).toBe(A); + } + + { + class A { + constructor (public readonly b: B) { + } + } + + class B { + constructor ( + public readonly target: TransientInjectionTarget + ) { + } + } + + expect(() => Injector.from([A, B])).toThrow(); + } + + { + class A { + constructor ( + public readonly target: TransientInjectionTarget + ) { + } + } + + const injector = Injector.from([{ provide: A, transient: true }]); + expect(() => injector.get(A)).toThrow(DependenciesUnmetError); + } + + { + class A { + constructor ( + public readonly target: TransientInjectionTarget + ) { + } + } + + const injector = Injector.from([ + { provide: A, transient: true, useFactory: (target: TransientInjectionTarget) => new A(target) } + ]); + expect(() => injector.get(A)).toThrow(DependenciesUnmetError); + } + + { + class A { + constructor ( + public readonly target?: TransientInjectionTarget + ) { + } + } + + const injector = Injector.from([{ provide: A, transient: true }]); + expect(() => injector.get(A)).not.toThrow(); + } +});