Skip to content

Commit

Permalink
feature(injector): TransientInjectorTarget injectable
Browse files Browse the repository at this point in the history
  • Loading branch information
timvandam committed Sep 20, 2023
1 parent 2ffaeb6 commit 29a8dbb
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 5 deletions.
45 changes: 42 additions & 3 deletions packages/injector/src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)[] = [];
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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')}
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<any> {
Expand Down
101 changes: 99 additions & 2 deletions packages/injector/tests/injector.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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();
}
});

0 comments on commit 29a8dbb

Please sign in to comment.