diff --git a/spec/auth-guard.test.ts b/spec/auth-guard.test.ts index 415113ca..b3191017 100644 --- a/spec/auth-guard.test.ts +++ b/spec/auth-guard.test.ts @@ -3,7 +3,6 @@ import { DanetApplication } from '../src/app.ts'; import { GLOBAL_GUARD } from '../src/guard/constants.ts'; import { UseGuard } from '../src/guard/decorator.ts'; import { AuthGuard } from '../src/guard/interface.ts'; -import { TokenInjector } from '../src/injector/injectable/constructor.ts'; import { Injectable } from '../src/injector/injectable/decorator.ts'; import { Module } from '../src/module/decorator.ts'; import { Controller, Get } from '../src/router/controller/decorator.ts'; @@ -133,7 +132,7 @@ class GlobalAuthController { @Module({ imports: [], controllers: [GlobalAuthController], - injectables: [new TokenInjector(GlobalGuard, GLOBAL_GUARD), SimpleService], + injectables: [{ useClass: GlobalGuard, token: GLOBAL_GUARD }, SimpleService], }) class GlobalAuthModule {} diff --git a/spec/injection.test.ts b/spec/injection.test.ts index 444b2036..d1010582 100644 --- a/spec/injection.test.ts +++ b/spec/injection.test.ts @@ -8,7 +8,6 @@ import { DanetApplication } from '../src/app.ts'; import { GLOBAL_GUARD } from '../src/guard/constants.ts'; import { AuthGuard } from '../src/guard/interface.ts'; import { Inject } from '../src/injector/decorator.ts'; -import { TokenInjector } from '../src/injector/injectable/constructor.ts'; import { Injectable, SCOPE } from '../src/injector/injectable/decorator.ts'; import { Module, ModuleOptions } from '../src/module/decorator.ts'; import { Controller, Get, Post } from '../src/router/controller/decorator.ts'; @@ -22,6 +21,11 @@ Deno.test('Injection', async (testContext) => { id: string; } + interface ConfigurationObject { + name: string; + port: number; + } + @Injectable() class GlobalGuard implements AuthGuard { canActivate(context: HttpContext): boolean { @@ -81,11 +85,13 @@ Deno.test('Injection', async (testContext) => { constructor( public child2: GlobalInjectable, @Inject('DB_SERVICE') public dbService: IDBService, + @Inject('CONFIGURATION') public injectedPlainObject: ConfigurationObject, ) { } @Get('') getMethod() { + return `${this.injectedPlainObject.name} and ${this.injectedPlainObject.port}`; } @Post('/post/') @@ -100,7 +106,17 @@ Deno.test('Injection', async (testContext) => { controllers: [SingletonController], injectables: [ GlobalInjectable, - new TokenInjector(DatabaseService, 'DB_SERVICE'), + { + token: 'DB_SERVICE', + useClass: DatabaseService, + }, + { + token: 'CONFIGURATION', + useValue: { + name: 'toto', + port: '4000', + }, + }, ], }; } @@ -112,7 +128,10 @@ Deno.test('Injection', async (testContext) => { injectables: [ Child1, GlobalInjectable, - new TokenInjector(GlobalGuard, GLOBAL_GUARD), + { + token: GLOBAL_GUARD, + useClass: GlobalGuard, + }, ], }; @@ -134,7 +153,7 @@ Deno.test('Injection', async (testContext) => { assertRejects(() => failingApp.init(ModuleWithMissingProvider)); }, ); - + const app = new DanetApplication(); await app.init(FirstModule); @@ -183,5 +202,13 @@ Deno.test('Injection', async (testContext) => { assertInstanceOf(globalGuard, GlobalGuard); }); - + await testContext.step( + 'inject plain object when using useValue', + async () => { + const firstInstance = await app.get( + SingletonController, + )!; + assertEquals(firstInstance.getMethod(), 'toto and 4000'); + }, + ); }); diff --git a/spec/lifecycle-hook.test.ts b/spec/lifecycle-hook.test.ts index ab38971c..7ae533a8 100644 --- a/spec/lifecycle-hook.test.ts +++ b/spec/lifecycle-hook.test.ts @@ -6,7 +6,6 @@ import { Module } from '../src/module/decorator.ts'; import { Controller } from '../src/router/controller/decorator.ts'; Deno.test('Lifecycle hooks', async (testContext) => { - let moduleOnAppBootstrapCalled = false; @Injectable({ scope: SCOPE.GLOBAL }) @@ -45,7 +44,6 @@ Deno.test('Lifecycle hooks', async (testContext) => { ], }) class MyModule implements OnAppBootstrap { - onAppBootstrap(): void | Promise { moduleOnAppBootstrapCalled = true; } @@ -62,11 +60,9 @@ Deno.test('Lifecycle hooks', async (testContext) => { }, ); - await testContext.step('call module onAppBoostrap hook', - () => { + await testContext.step('call module onAppBoostrap hook', () => { assertEquals(moduleOnAppBootstrapCalled, true); - } - ) + }); await testContext.step( 'call global controller onAppBootstrap hook', diff --git a/spec/scoped-lifecycle-hook-another-order.test.ts b/spec/scoped-lifecycle-hook-another-order.test.ts index 78415340..245d8b58 100644 --- a/spec/scoped-lifecycle-hook-another-order.test.ts +++ b/spec/scoped-lifecycle-hook-another-order.test.ts @@ -6,7 +6,6 @@ import { Module } from '../src/module/decorator.ts'; import { Controller, Get } from '../src/router/controller/decorator.ts'; import { HttpContext } from '../src/router/router.ts'; import { Inject } from '../src/injector/decorator.ts'; -import { TokenInjector } from '../src/injector/injectable/constructor.ts'; Deno.test('Scoped Lifecycle hooks other order', async (testContext) => { interface ScopedInjectableInterface { @@ -59,7 +58,10 @@ Deno.test('Scoped Lifecycle hooks other order', async (testContext) => { @Module({ controllers: [ScopedController, SideEffectController], injectables: [ - new TokenInjector(ScopedInjectable, 'SCOPED_TOKEN'), + { + token: 'SCOPED_TOKEN', + useClass: ScopedInjectable, + }, InjectableUsingScoped, ], }) diff --git a/spec/scoped-lifecycle-hook.test.ts b/spec/scoped-lifecycle-hook.test.ts index 671314b3..9d1a1358 100644 --- a/spec/scoped-lifecycle-hook.test.ts +++ b/spec/scoped-lifecycle-hook.test.ts @@ -6,7 +6,6 @@ import { Module } from '../src/module/decorator.ts'; import { Controller, Get } from '../src/router/controller/decorator.ts'; import { HttpContext } from '../src/router/router.ts'; import { Inject } from '../src/injector/decorator.ts'; -import { TokenInjector } from '../src/injector/injectable/constructor.ts'; Deno.test('Scoped Lifecycle hooks', async (testContext) => { interface ScopedInjectableInterface { @@ -60,7 +59,10 @@ Deno.test('Scoped Lifecycle hooks', async (testContext) => { controllers: [ScopedController, SideEffectController], injectables: [ InjectableUsingScoped, - new TokenInjector(ScopedInjectable, 'SCOPED_TOKEN'), + { + useClass: ScopedInjectable, + token: 'SCOPED_TOKEN', + }, ], }) class ParentBeforeScopedModule {} diff --git a/src/app.ts b/src/app.ts index e113d7c7..1090196b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -52,21 +52,24 @@ export class DanetApplication { // deno-lint-ignore no-explicit-any const possibleModuleInstance = ModuleInstanceOrConstructor as any; let instance: ModuleInstance; - - if (!(possibleModuleInstance).imports - && !(possibleModuleInstance).injectables) { - instance = new (ModuleInstanceOrConstructor as Constructor)() as ModuleInstance; - const metadata: ModuleOptions = MetadataHelper.getMetadata( - moduleMetadataKey, - ModuleInstanceOrConstructor, - ); - instance.controllers = metadata.controllers; - instance.imports = metadata.imports; - instance.injectables = metadata.injectables; + + if ( + !possibleModuleInstance.imports && + !possibleModuleInstance.injectables + ) { + instance = + new (ModuleInstanceOrConstructor as Constructor)() as ModuleInstance; + const metadata: ModuleOptions = MetadataHelper.getMetadata( + moduleMetadataKey, + ModuleInstanceOrConstructor, + ); + instance.controllers = metadata.controllers; + instance.imports = metadata.imports; + instance.injectables = metadata.injectables; } else { instance = ModuleInstanceOrConstructor as ModuleInstance; } - + for (const module in instance?.imports) { // deno-lint-ignore no-explicit-any await this.bootstrap(instance.imports[module as any]); diff --git a/src/events/module.ts b/src/events/module.ts index 5b7a4cf1..64a684a4 100644 --- a/src/events/module.ts +++ b/src/events/module.ts @@ -23,9 +23,11 @@ export class EventEmitterModule implements OnAppBootstrap, OnAppClose { emitter.unsubscribe(); } - // deno-lint-ignore no-explicit-any + // deno-lint-ignore no-explicit-any private registerAvailableEventListeners(injectableInstance: any) { - const methods = Object.getOwnPropertyNames(injectableInstance.constructor.prototype); + const methods = Object.getOwnPropertyNames( + injectableInstance.constructor.prototype, + ); const emitter = injector.get(EventEmitter); for (const method of methods) { diff --git a/src/injector/injectable/constructor.ts b/src/injector/injectable/constructor.ts index 06b1ccd6..657038c7 100644 --- a/src/injector/injectable/constructor.ts +++ b/src/injector/injectable/constructor.ts @@ -1,7 +1,12 @@ import { Constructor } from '../../utils/constructor.ts'; export type InjectableConstructor = Constructor; -export class TokenInjector { - constructor(public useClass: InjectableConstructor, public token: string) { - } -} +export type UseClassInjector = { + useClass: InjectableConstructor; + token: string; +}; +export type UseValueInjector = { + // deno-lint-ignore no-explicit-any + useValue: any; + token: string; +}; diff --git a/src/injector/injector.ts b/src/injector/injector.ts index de58edef..49c79796 100644 --- a/src/injector/injector.ts +++ b/src/injector/injector.ts @@ -6,7 +6,8 @@ import { Constructor } from '../utils/constructor.ts'; import { getInjectionTokenMetadataKey } from './decorator.ts'; import { InjectableConstructor, - TokenInjector, + UseClassInjector, + UseValueInjector, } from './injectable/constructor.ts'; import { InjectableOption, @@ -66,21 +67,21 @@ export class Injector { } public addAvailableInjectable( - injectables: (InjectableConstructor | TokenInjector)[], + injectables: + (InjectableConstructor | UseClassInjector | UseValueInjector)[], ) { for (const injectable of injectables) { - const actualKey = injectable instanceof TokenInjector - ? injectable.token - : injectable; - const actualType = injectable instanceof TokenInjector - ? injectable.useClass - : injectable; - this.availableTypes.set(actualKey, actualType); + const { actualKey, actualType, instance } = this.getKeyAndTypeOrInstance( + injectable, + ); + this.availableTypes.set(actualKey, actualType ?? instance); } } public async registerInjectables( - Injectables: Array, + Injectables: Array< + InjectableConstructor | UseClassInjector | UseValueInjector + >, ) { for (const Provider of Injectables) { await this.resolveInjectable(Provider); @@ -89,13 +90,13 @@ export class Injector { public async resolveControllers(Controllers: ControllerConstructor[]) { for (const Controller of Controllers) { - await this.resolveControllerDependencies(Controller); + await this.resolveControllerParameters(Controller); } } - private async resolveControllerDependencies(Type: Constructor) { + private async resolveControllerParameters(Type: Constructor) { let canBeSingleton = true; - const dependencies = this.getDependencies(Type); + const dependencies = this.getParametersTypes(Type); dependencies.forEach((DependencyType, idx) => { const actualType = this.getParamToken(Type, idx) ?? DependencyType; if (!this.resolved.has(actualType)) { @@ -132,21 +133,29 @@ export class Injector { } private async resolveInjectable( - Type: InjectableConstructor | TokenInjector, + Type: InjectableConstructor | UseClassInjector | UseValueInjector, ParentConstructor?: Constructor, token?: string, ) { - const actualType = Type instanceof TokenInjector ? Type.useClass : Type; - const actualKey = Type instanceof TokenInjector - ? Type.token - : (token ?? Type); - const dependencies = this.getDependencies(actualType); + const { actualType, actualKey, instance } = this.getKeyAndTypeOrInstance( + Type, + token, + ); + if (!actualType) { + this.resolved.set(actualKey, () => instance); + this.resolvedTypes.set(actualKey, instance); + this.injectables.push(instance); + return; + } + const parameters = this.getParametersTypes(actualType); - if (this.resolved.has(actualType)) { + if (this.resolved.has(actualType ?? instance)) { return; } - await this.resolveDependencies(dependencies, actualType); + if (parameters.length > 0) { + await this.resolveDependencies(parameters, actualType); + } const injectableMetadata = MetadataHelper.getMetadata( injectionData, actualType, @@ -155,7 +164,7 @@ export class Injector { let canBeSingleton = injectableMetadata?.scope !== SCOPE.REQUEST && injectableMetadata?.scope !== SCOPE.TRANSIENT; if (canBeSingleton) { - for (const [idx, Dep] of dependencies.entries()) { + for (const [idx, Dep] of parameters.entries()) { const token = this.getParamToken(actualType, idx); const type = this.resolvedTypes.get(token ?? Dep); const dependencyInjectableMetadata = MetadataHelper.getMetadata< @@ -173,15 +182,15 @@ export class Injector { } } if (canBeSingleton) { - const resolvedDependencies = new Array(); - for (const [idx, Dep] of dependencies.entries()) { - resolvedDependencies.push( + const resolvedParameters = new Array(); + for (const [idx, Dep] of parameters.entries()) { + resolvedParameters.push( await (this.resolved.get( this.getParamToken(actualType, idx) ?? Dep, )!()) as Constructor, ); } - const instance = new actualType(...resolvedDependencies); + const instance = new actualType(...resolvedParameters); this.resolved.set(actualKey, () => instance); this.resolvedTypes.set(actualKey, actualType); this.injectables.push(instance); @@ -206,12 +215,30 @@ export class Injector { this.setNonSingleton( actualType, actualKey, - dependencies, + parameters, injectableMetadata?.scope === SCOPE.TRANSIENT, ); } } + private getKeyAndTypeOrInstance( + Type: InjectableConstructor | UseValueInjector | UseClassInjector, + token?: string | undefined, + ) { + if (Object.hasOwn(Type, 'token')) { + const actualKey = (Type as UseClassInjector).token; + if (Object.hasOwn(Type, 'useClass')) { + return { actualKey, actualType: (Type as UseClassInjector).useClass }; + } else if (Object.hasOwn(Type, 'useValue')) { + return { actualKey, instance: (Type as UseValueInjector).useValue }; + } + } + return { + actualKey: (token ?? Type as InjectableConstructor), + actualType: Type as InjectableConstructor, + }; + } + private getParamToken(Type: Constructor, paramIndex: number) { return MetadataHelper.getMetadata( getInjectionTokenMetadataKey(paramIndex), @@ -277,7 +304,7 @@ export class Injector { } } - private getDependencies(Type: Constructor): Constructor[] { + private getParametersTypes(Type: Constructor): Constructor[] { return MetadataHelper.getMetadata('design:paramtypes', Type) || []; } diff --git a/src/module/decorator.ts b/src/module/decorator.ts index 2241ee7f..9288d8db 100644 --- a/src/module/decorator.ts +++ b/src/module/decorator.ts @@ -1,22 +1,20 @@ import { MetadataHelper } from '../metadata/helper.ts'; import { ControllerConstructor } from '../router/controller/constructor.ts'; -import { - InjectableConstructor, - TokenInjector, -} from '../injector/injectable/constructor.ts'; +import { InjectableConstructor } from '../injector/injectable/constructor.ts'; import { Constructor } from '../utils/constructor.ts'; import { ModuleConstructor } from './constructor.ts'; +import { UseClassInjector, UseValueInjector } from '../mod.ts'; export class ModuleOptions { imports?: Array = []; controllers?: ControllerConstructor[] = []; - injectables?: Array = []; + injectables?: Array< + InjectableConstructor | UseValueInjector | UseClassInjector + > = []; } - export class ModuleInstance extends ModuleOptions {} - export const moduleMetadataKey = 'module'; export function Module(options: ModuleOptions) { diff --git a/src/utils/serve-static.ts b/src/utils/serve-static.ts index ca1e282d..fa6bfce9 100644 --- a/src/utils/serve-static.ts +++ b/src/utils/serve-static.ts @@ -29,10 +29,10 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }) => { if (!path) return await next(); - if (Deno.build.os !== "windows") { + if (Deno.build.os !== 'windows') { path = `/${path}`; } - + let file; try {