Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransientInjectorTarget & ScopedLogger #483

Merged
merged 4 commits into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading