diff --git a/packages/framework/src/module.ts b/packages/framework/src/module.ts index a868ab6c9..44ad52f77 100644 --- a/packages/framework/src/module.ts +++ b/packages/framework/src/module.ts @@ -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, @@ -50,6 +51,7 @@ export class FrameworkModule extends createModule({ RpcServer, ConsoleTransport, Logger, + ScopedLogger, MigrationProvider, DebugController, { provide: DatabaseRegistry, useFactory: (ic: InjectorContext) => new DatabaseRegistry(ic) }, @@ -105,6 +107,7 @@ export class FrameworkModule extends createModule({ MigrationCreateController, ], exports: [ + ScopedLogger.provide, ProcessLocker, ApplicationServer, WebWorkerFactory, diff --git a/packages/injector/src/injector.ts b/packages/injector/src/injector.ts index 6f5625de4..cbdb19c4c 100644 --- a/packages/injector/src/injector.ts +++ b/packages/injector/src/injector.ts @@ -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)[] = []; @@ -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. @@ -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[] = []; @@ -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); } `); @@ -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')} } @@ -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[] = []; @@ -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(', ')});`; @@ -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)}`); } @@ -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); } @@ -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); @@ -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); } @@ -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(', ')}]))`; } @@ -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 { diff --git a/packages/injector/src/provider.ts b/packages/injector/src/provider.ts index 6e0210b0a..36b2d4669 100644 --- a/packages/injector/src/provider.ts +++ b/packages/injector/src/provider.ts @@ -22,7 +22,18 @@ export interface ProviderBase { /** @reflection never */ export type Token = symbol | number | bigint | RegExp | boolean | string | AbstractClassType | Type | T; -export function provide(provider: { useValue: T } | { useClass: ClassType } | { useExisting: any } | { useFactory: (...args: any[]) => T } | ClassType, type?: ReceiveType): NormalizedProvider { +export function provide( + provider: + | (ProviderBase & + ( + | { useValue: T } + | { useClass: ClassType } + | { useExisting: any } + | { useFactory: (...args: any[]) => T } + )) + | ClassType, + type?: ReceiveType, +): NormalizedProvider { if (isClass(provider)) return { provide: resolveReceiveType(type), useClass: provider }; return { ...provider, provide: resolveReceiveType(type) }; } diff --git a/packages/injector/tests/injector.spec.ts b/packages/injector/tests/injector.spec.ts index 7ebf220dc..ffe916faa 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,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({ transient: true, useFactory: (target: TransientInjectionTarget): B => ({ target }) }), + ]); + + const a = injector.get(A); + expect(a.b).toBeDefined(); + expect(a.b.target.token).toBe(A); + } +}); diff --git a/packages/logger/package.json b/packages/logger/package.json index 14e249491..ba13810ef 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -24,7 +24,8 @@ "author": "Marc J. Schmidt ", "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", @@ -32,7 +33,8 @@ "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", diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 73c441baa..5500d1a8a 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -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, @@ -309,3 +310,11 @@ export class Logger implements LoggerInterface { this.send(message, LoggerLevel.debug); } } + +export type ScopedLogger = Inject; +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; diff --git a/packages/logger/tests/logger.spec.ts b/packages/logger/tests/logger.spec.ts index 455f1e612..9f53b98c6 100644 --- a/packages/logger/tests/logger.spec.ts +++ b/packages/logger/tests/logger.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@jest/globals'; -import { JSONTransport, Logger, LoggerLevel, ScopeFormatter } from '../src/logger.js'; +import { JSONTransport, Logger, LoggerLevel, ScopedLogger, ScopeFormatter } from '../src/logger.js'; import { MemoryLoggerTransport } from '../src/memory-logger.js'; +import { Injector, ServiceNotFoundError, TransientInjectionTarget } from '@deepkit/injector'; test('log level', () => { const logger = new Logger(); @@ -79,3 +80,86 @@ test('colorless', () => { logger.log('This is a color test'); }); + +test('scoped logger', () => { + class MyProvider { + constructor (public logger: ScopedLogger) { + } + } + + { + const injector = Injector.from([ + MyProvider, + Logger, // optional base logger used by ScopedLogger + ScopedLogger, + ]); + const logger = injector.get(Logger); + const provider = injector.get(MyProvider); + expect(logger).toBeInstanceOf(Logger); + expect(provider.logger).toBeInstanceOf(Logger); + expect(provider.logger).toBe(logger.scoped('MyProvider')); + } + + { + const injector = Injector.from([ + MyProvider, + ScopedLogger, + ]); + expect(() => injector.get(Logger)).toThrow(ServiceNotFoundError); + const provider = injector.get(MyProvider); + expect(provider.logger).toBeInstanceOf(Logger); + } + + { + class A { + constructor (public b: B) { + } + } + + class B { + constructor (public c: C, public target: TransientInjectionTarget) { + } + } + + class C { + constructor (public target: TransientInjectionTarget) { + } + } + + const injector = Injector.from([ + A, + { provide: B, transient: true }, + { provide: C, transient: true }, + ]); + const a = injector.get(A); + expect(a.b.c.target.token).toBe(B); + expect(a.b.target.token).toBe(A); + } + + { + 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); + } +});