Skip to content

Commit

Permalink
TransientInjectorTarget & ScopedLogger (#483)
Browse files Browse the repository at this point in the history
* feature(injector): TransientInjectorTarget injectable

* feature(logger): ScopedLogger provider

* fix(injector): make TransientInjectionTarget work with useExisting/cross-module

* feature(framework): include ScopedLogger in FrameworkModule
  • Loading branch information
timvandam committed Oct 1, 2023
1 parent aa25c5b commit e90f02c
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 15 deletions.
3 changes: 3 additions & 0 deletions packages/framework/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ApiConsoleModule } from '@deepkit/api-console-module';
import { AppModule, ControllerConfig, createModule } from '@deepkit/app';
import { RpcControllers, RpcInjectorContext, RpcKernelWithStopwatch } from './rpc.js';
import { normalizeDirectory } from './utils.js';
import { ScopedLogger } from "@deepkit/logger";

export class FrameworkModule extends createModule({
config: FrameworkConfig,
Expand All @@ -50,6 +51,7 @@ export class FrameworkModule extends createModule({
RpcServer,
ConsoleTransport,
Logger,
ScopedLogger,
MigrationProvider,
DebugController,
{ provide: DatabaseRegistry, useFactory: (ic: InjectorContext) => new DatabaseRegistry(ic) },
Expand Down Expand Up @@ -105,6 +107,7 @@ export class FrameworkModule extends createModule({
MigrationCreateController,
],
exports: [
ScopedLogger.provide,
ProcessLocker,
ApplicationServer,
WebWorkerFactory,
Expand Down
59 changes: 50 additions & 9 deletions packages/injector/src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ 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 into other providers`
);
}

type Destination = { token: Token; };
function createTransientInjectionTarget(destination: Destination | undefined) {
if (!destination) {
return undefined;
}

return new TransientInjectionTarget(destination.token);
}


let CircularDetector: any[] = [];
let CircularDetectorResets: (() => void)[] = [];
Expand Down Expand Up @@ -198,6 +213,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 +294,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('createTransientInjectionTarget', createTransientInjectionTarget);
resolverCompiler.context.set('injector', this);

const lines: string[] = [];
Expand Down Expand Up @@ -310,7 +339,7 @@ export class Injector implements InjectorInterface {
//its a redirect
lines.push(`
case token === ${resolverCompiler.reserveConst(prepared.token, 'token')}${scopeObjectCheck}: {
return ${resolverCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.resolver(${resolverCompiler.reserveConst(prepared.token, 'token')}, scope);
return ${resolverCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.resolver(${resolverCompiler.reserveConst(prepared.token, 'token')}, scope, destination);
}
`);

Expand Down Expand Up @@ -355,7 +384,7 @@ export class Injector implements InjectorInterface {
${resets.join('\n')};
});
return function(token, scope) {
return function(token, scope, destination) {
switch (true) {
${lines.join('\n')}
}
Expand Down Expand Up @@ -396,7 +425,7 @@ export class Injector implements InjectorInterface {
factory = this.createFactory(provider, accessor, compiler, useClass, resolveDependenciesFrom);
} else if (isExistingProvider(provider)) {
transient = provider.transient === true;
factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(provider.useExisting)}, scope)`;
factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(provider.useExisting)}, scope, destination)`;
} else if (isFactoryProvider(provider)) {
transient = provider.transient === true;
const args: string[] = [];
Expand All @@ -405,11 +434,12 @@ export class Injector implements InjectorInterface {
for (const parameter of reflection.getParameters()) {
factory.dependencies++;
const tokenType = getInjectOptions(parameter.getType() as Type);
const ofName = reflection.name === 'anonymous' ? 'useFactory' : reflection.name;
args.push(this.createFactoryProperty({
name: parameter.name,
type: tokenType || parameter.getType() as Type,
optional: !parameter.isValueRequired()
}, provider, compiler, resolveDependenciesFrom, reflection.name || 'useFactory', args.length, 'factoryDependencyNotFound'));
}, provider, compiler, resolveDependenciesFrom, ofName, args.length, 'factoryDependencyNotFound'));
}

factory.code = `${accessor} = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`;
Expand All @@ -435,7 +465,7 @@ export class Injector implements InjectorInterface {
for (const arg of call.args) {
if (arg instanceof InjectorReference) {
const injector = arg.module ? compiler.reserveConst(arg.module) + '.injector' : 'injector';
args.push(`${injector}.resolver(${compiler.reserveConst(arg.to)}, scope)`);
args.push(`${injector}.resolver(${compiler.reserveConst(arg.to)}, scope, destination)`);
} else {
args.push(`${compiler.reserveVariable('arg', arg)}`);
}
Expand All @@ -448,7 +478,7 @@ export class Injector implements InjectorInterface {
let value: string = '';
if (call.value instanceof InjectorReference) {
const injector = call.value.module ? compiler.reserveConst(call.value.module) + '.injector' : 'injector';
value = `${injector}.resolver(${compiler.reserveConst(call.value.to)}, scope)`;
value = `${injector}.resolver(${compiler.reserveConst(call.value.to)}, scope, destination)`;
} else {
value = compiler.reserveVariable('value', call.value);
}
Expand Down Expand Up @@ -544,6 +574,7 @@ export class Injector implements InjectorInterface {
notFoundFunction: string
): string {
let of = `${ofName}.${options.name}`;
const destinationVar = compiler.reserveConst({ token: fromProvider.provide });

if (options.type.kind === ReflectionKind.class) {
const found = findModuleForConfig(options.type.classType, resolveDependenciesFrom);
Expand All @@ -552,6 +583,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 `createTransientInjectionTarget(destination) ${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 All @@ -566,7 +607,7 @@ export class Injector implements InjectorInterface {
const entries = this.buildContext.tagRegistry.resolve(options.type.classType);
const args: string[] = [];
for (const entry of entries) {
args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(entry.tagProvider.provider.provide)}, scope)`);
args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(entry.tagProvider.provider.provide)}, scope, ${destinationVar})`);
}
return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`;
}
Expand Down Expand Up @@ -684,9 +725,9 @@ export class Injector implements InjectorInterface {

const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0];
if (resolveFromModule === this.module) {
return `injector.resolver(${tokenVar}, scope)`;
return `injector.resolver(${tokenVar}, scope, ${destinationVar})`;
}
return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope) ${orThrow}`;
return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope, ${destinationVar}) ${orThrow}`;
}

createResolver(type: Type, scope?: Scope, label?: string): Resolver<any> {
Expand Down
13 changes: 12 additions & 1 deletion packages/injector/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ export interface ProviderBase {
/** @reflection never */
export type Token<T = any> = symbol | number | bigint | RegExp | boolean | string | AbstractClassType<T> | Type | T;

export function provide<T>(provider: { useValue: T } | { useClass: ClassType } | { useExisting: any } | { useFactory: (...args: any[]) => T } | ClassType, type?: ReceiveType<T>): NormalizedProvider {
export function provide<T>(
provider:
| (ProviderBase &
(
| { useValue: T }
| { useClass: ClassType }
| { useExisting: any }
| { useFactory: (...args: any[]) => T }
))
| ClassType,
type?: ReceiveType<T>,
): NormalizedProvider {
if (isClass(provider)) return { provide: resolveReceiveType(type), useClass: provider };
return { ...provider, provide: resolveReceiveType(type) };
}
Expand Down
147 changes: 145 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,146 @@ 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();
}

{
class A {
constructor (public b: C) {
}
}

class B {
constructor (public target: TransientInjectionTarget) {
}
}

class C {
constructor (public target: TransientInjectionTarget) {
}
}

const injector = Injector.from([
A,
{ provide: B, transient: true },
{ provide: C, transient: true, useExisting: B },
]);
const a = injector.get(A);
expect(a.b).toBeInstanceOf(B);
expect(a.b.target.token).toBe(A);
}

{
class A {
constructor (public b: B) {
}
}

interface B {
target: TransientInjectionTarget;
}

const injector = Injector.from([
A,
provide<B>({ transient: true, useFactory: (target: TransientInjectionTarget): B => ({ target }) }),
]);

const a = injector.get(A);
expect(a.b).toBeDefined();
expect(a.b.target.token).toBe(A);
}
});
6 changes: 4 additions & 2 deletions packages/logger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@
"author": "Marc J. Schmidt <[email protected]>",
"license": "MIT",
"peerDependencies": {
"@deepkit/core": "^1.0.1-alpha.13"
"@deepkit/core": "^1.0.1-alpha.13",
"@deepkit/injector": "^1.0.1-alpha.97"
},
"dependencies": {
"@types/format-util": "^1.0.1",
"ansi-styles": "^4.3.0",
"format-util": "^1.0.5"
},
"devDependencies": {
"@deepkit/core": "^1.0.1-alpha.97"
"@deepkit/core": "^1.0.1-alpha.97",
"@deepkit/injector": "^1.0.1-alpha.97"
},
"jest": {
"testEnvironment": "node",
Expand Down
9 changes: 9 additions & 0 deletions packages/logger/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import style from 'ansi-styles';
import format from 'format-util';
import { arrayRemoveItem, ClassType } from '@deepkit/core';
import { FactoryProvider, Inject, TransientInjectionTarget } from '@deepkit/injector';

export enum LoggerLevel {
none,
Expand Down Expand Up @@ -309,3 +310,11 @@ export class Logger implements LoggerInterface {
this.send(message, LoggerLevel.debug);
}
}

export type ScopedLogger = Inject<Logger, 'scoped-logger'>;
export const ScopedLogger = {
provide: 'scoped-logger',
transient: true,
useFactory: (target: TransientInjectionTarget, logger: Logger = new Logger()) =>
logger.scoped(target.token?.name ?? String(target.token)),
} as const;
Loading

0 comments on commit e90f02c

Please sign in to comment.