diff --git a/packages/app/tests/module.spec.ts b/packages/app/tests/module.spec.ts index def632a25..36ea6cd1b 100644 --- a/packages/app/tests/module.spec.ts +++ b/packages/app/tests/module.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@jest/globals'; import { Minimum, MinLength } from '@deepkit/type'; -import { injectorReference, provide } from '@deepkit/injector'; +import { provide } from '@deepkit/injector'; import { ServiceContainer } from '../src/service-container.js'; import { ClassType } from '@deepkit/core'; import { AppModule, createModule } from '../src/module.js'; @@ -151,6 +151,7 @@ test('configured provider', () => { addTransport(transport: any) { this.transporter.push(transport); + return this; } } @@ -164,7 +165,7 @@ test('configured provider', () => { { const module = new AppModule(); const logger = new ServiceContainer(module.setup((module) => { - module.setupProvider().addTransport('first').addTransport('second'); + module.configureProvider(v => v.addTransport('first').addTransport('second')); })).getInjector(module).get(Logger); expect(logger.transporter).toEqual(['first', 'second']); } @@ -172,7 +173,7 @@ test('configured provider', () => { { const module = new AppModule(); const logger = new ServiceContainer(module.setup((module) => { - module.setupProvider().transporter = ['first', 'second', 'third']; + module.configureProvider(v => v.transporter = ['first', 'second', 'third']); })).getInjector(module).get(Logger); expect(logger.transporter).toEqual(['first', 'second', 'third']); } @@ -180,7 +181,7 @@ test('configured provider', () => { { const module = new AppModule(); const logger = new ServiceContainer(module.setup((module) => { - module.setupProvider().addTransport(new Transporter); + module.configureProvider(v => v.addTransport(new Transporter)); })).getInjector(module).get(Logger); expect(logger.transporter[0] instanceof Transporter).toBe(true); } @@ -188,7 +189,7 @@ test('configured provider', () => { { const module = new AppModule(); const logger = new ServiceContainer(module.setup((module) => { - module.setupProvider().addTransport(injectorReference(Transporter)); + module.configureProvider((v, t: Transporter) => v.addTransport(t)); })).getInjector(module).get(Logger); expect(logger.transporter[0] instanceof Transporter).toBe(true); } diff --git a/packages/example-app/app.ts b/packages/example-app/app.ts index 7190acd46..8cdc35448 100755 --- a/packages/example-app/app.ts +++ b/packages/example-app/app.ts @@ -55,10 +55,11 @@ app.setup((module, config) => { module.getImportedModuleByClass(FrameworkModule).configure({ debug: true }); } - if (config.environment === 'production') { - //enable logging JSON messages instead of formatted strings - module.setupGlobalProvider().setTransport([new JSONTransport]); - } + module.configureProvider(logger => { + if (config.environment === 'production') { + logger.setTransport([new JSONTransport]); + } + }); }); app.loadConfigFromEnv().run(); diff --git a/packages/framework/src/module.ts b/packages/framework/src/module.ts index 01e2c11b6..bb1c7ab6c 100644 --- a/packages/framework/src/module.ts +++ b/packages/framework/src/module.ts @@ -18,7 +18,7 @@ import { ServerStartController } from './cli/server-start.js'; import { DebugController } from './debug/debug.controller.js'; import { registerDebugHttpController } from './debug/http-debug.controller.js'; import { http, HttpLogger, HttpModule, HttpRequest, serveStaticListener } from '@deepkit/http'; -import { InjectorContext, injectorReference, ProviderWithScope, Token } from '@deepkit/injector'; +import { InjectorContext, ProviderWithScope, Token } from '@deepkit/injector'; import { BrokerConfig, FrameworkConfig } from './module.config.js'; import { LoggerInterface } from '@deepkit/logger'; import { SessionHandler } from './session.js'; @@ -35,7 +35,7 @@ import { DebugConfigController } from './cli/app-config.js'; import { Zone } from './zone.js'; import { DebugBrokerBus } from './debug/broker.js'; import { ApiConsoleModule } from '@deepkit/api-console-module'; -import { AppModule, ControllerConfig, createModule, onAppExecute, onAppShutdown } from '@deepkit/app'; +import { AppModule, ControllerConfig, createModule, onAppShutdown } from '@deepkit/app'; import { RpcControllers, RpcInjectorContext, RpcKernelWithStopwatch } from './rpc.js'; import { normalizeDirectory } from './utils.js'; import { FilesystemRegistry, PublicFilesystem } from './filesystem.js'; @@ -164,8 +164,8 @@ export class FrameworkModule extends createModule({ this.addImport(); this.addProvider({ provide: RpcControllers, useValue: this.rpcControllers }); - this.setupProvider().setMigrationDir(this.config.migrationDir); - this.setupProvider().setMigrateOnStartup(this.config.migrateOnStartup); + this.configureProvider(v => v.setMigrationDir(this.config.migrationDir)); + this.configureProvider(v => v.setMigrateOnStartup(this.config.migrateOnStartup)); if (this.config.httpLog) { this.addListener(HttpLogger); @@ -217,35 +217,33 @@ export class FrameworkModule extends createModule({ this.addProvider(DebugBrokerBus); this.addProvider({ provide: StopwatchStore, useClass: FileStopwatchStore }); - const stopwatch = this.setupGlobalProvider(); - if (this.config.profile || this.config.debug) { - stopwatch.enable(); - } else { - stopwatch.disable(); - } - + const stopwatch = this.configureProvider(stopwatch => { + if (this.config.profile || this.config.debug) { + stopwatch.enable(); + } else { + stopwatch.disable(); + } + }, { global: true }); this.addExport(DebugBrokerBus, StopwatchStore); } postProcess() { //all providers are known at this point this.setupDatabase(); - - for (const fs of this.filesystems) { - this.setupProvider().addFilesystem(fs.classType, fs.module); - } + this.configureProvider(v => { + for (const fs of this.filesystems) { + v.addFilesystem(fs.classType, fs.module); + } + }); } protected setupDatabase() { for (const db of this.dbs) { - this.setupProvider().addDatabase(db.classType, {}, db.module); - db.module.setupProvider(0, db.classType).eventDispatcher = injectorReference(EventDispatcher); - } - - if (this.config.debug && this.config.profile) { - for (const db of this.dbs) { - db.module.setupProvider(0, db.classType).stopwatch = injectorReference(Stopwatch); - } + this.configureProvider(v => v.addDatabase(db.classType, {}, db.module)); + db.module.configureProvider((db: Database, eventDispatcher: EventDispatcher, stopwatch: Stopwatch) => { + db.eventDispatcher = eventDispatcher; + db.stopwatch = stopwatch; + }, {}, db.classType); } } diff --git a/packages/framework/src/testing.ts b/packages/framework/src/testing.ts index 23ee58f28..395abf229 100644 --- a/packages/framework/src/testing.ts +++ b/packages/framework/src/testing.ts @@ -14,7 +14,6 @@ import { ConsoleTransport, Logger, LogMessage, MemoryLoggerTransport } from '@de import { Database, DatabaseRegistry, MemoryDatabaseAdapter } from '@deepkit/orm'; import { ApplicationServer } from './application-server.js'; import { BrokerServer } from './broker/broker.js'; -import { injectorReference } from '@deepkit/injector'; import { App, AppModule, RootAppModule, RootModuleDefinition } from '@deepkit/app'; import { WebMemoryWorkerFactory, WebWorkerFactory } from './worker.js'; import { MemoryHttpResponse, RequestBuilder } from '@deepkit/http'; @@ -77,8 +76,8 @@ export class BrokerMemoryServer extends BrokerServer { export function createTestingApp(options: O, entities: ClassType[] = [], setup?: (module: AppModule) => void): TestingFacade> { const module = new RootAppModule(options); - module.setupGlobalProvider().removeTransport(injectorReference(ConsoleTransport)); - module.setupGlobalProvider().addTransport(injectorReference(MemoryLoggerTransport)); + module.configureProvider((v, t: ConsoleTransport) => v.removeTransport(t)); + module.configureProvider((v, t: MemoryLoggerTransport) => v.addTransport(t)); module.addProvider({ provide: WebWorkerFactory, useClass: WebMemoryWorkerFactory }); //don't start HTTP-server module.addProvider({ provide: BrokerServer, useExisting: BrokerMemoryServer }); //don't start Broker TCP-server @@ -100,7 +99,7 @@ export function createTestingApp(options: O, ent if (entities.length) { module.addProvider({ provide: Database, useValue: new Database(new MemoryDatabaseAdapter, entities) }); - module.setupGlobalProvider().addDatabase(Database, {}, module); + module.configureProvider(v => v.addDatabase(Database, {}, module)); } if (setup) module.setup(setup as any); diff --git a/packages/http/src/static-serving.ts b/packages/http/src/static-serving.ts index 64d15c743..37dd4812b 100644 --- a/packages/http/src/static-serving.ts +++ b/packages/http/src/static-serving.ts @@ -18,7 +18,7 @@ import { ClassType, urlJoin } from '@deepkit/core'; import { HttpRequest, HttpResponse } from './model.js'; import send from 'send'; import { eventDispatcher } from '@deepkit/event'; -import { RouteConfig, HttpRouter } from './router.js'; +import { HttpRouter, RouteConfig } from './router.js'; export function serveStaticListener(module: AppModule, path: string, localPath: string = path): ClassType { class HttpRequestStaticServingListener { @@ -48,9 +48,9 @@ export function serveStaticListener(module: AppModule, path: string, localP type: 'controller', controller: HttpRequestStaticServingListener, module, - methodName: 'serve' + methodName: 'serve', }), - () => ({arguments: [relativePath, event.request, event.response], parameters: {}}) + () => ({ arguments: [relativePath, event.request, event.response], parameters: {} }), ); } resolve(undefined); @@ -132,20 +132,20 @@ export function registerStaticHttpController(module: AppModule, options: St type: 'controller', controller: StaticController, module, - methodName: 'serveIndex' + methodName: 'serveIndex', }); route1.groups = groups; - module.setupGlobalProvider().addRoute(route1); + module.configureProvider(router => router.addRoute(route1), { global: true }); if (path !== '/') { const route2 = new RouteConfig('static', ['GET'], path.slice(0, -1), { type: 'controller', controller: StaticController, module, - methodName: 'serveIndex' + methodName: 'serveIndex', }); route2.groups = groups; - module.setupGlobalProvider().addRoute(route2); + module.configureProvider(router => router.addRoute(route2), { global: true }); } module.addProvider(StaticController); diff --git a/packages/injector/src/injector.ts b/packages/injector/src/injector.ts index 8d99d3c5d..e9215c09e 100644 --- a/packages/injector/src/injector.ts +++ b/packages/injector/src/injector.ts @@ -8,12 +8,11 @@ import { Tag, TagProvider, TagRegistry, - Token + Token, } from './provider.js'; import { AbstractClassType, ClassType, CompilerContext, CustomError, getClassName, getPathValue, isArray, isClass, isFunction, isPrototypeOfBase } from '@deepkit/core'; -import { findModuleForConfig, getScope, InjectorModule, PreparedProvider } from './module.js'; +import { findModuleForConfig, getScope, InjectorModule, PreparedProvider, ConfigurationProviderRegistry } from './module.js'; import { - hasTypeInformation, isExtendable, isOptional, isType, @@ -26,7 +25,7 @@ import { ReflectionKind, resolveReceiveType, stringifyType, - Type + Type, } from '@deepkit/type'; export class InjectorError extends CustomError { @@ -42,15 +41,6 @@ export class ServiceNotFoundError extends InjectorError { export class DependenciesUnmetError extends InjectorError { } -export class InjectorReference { - constructor(public readonly to: any, public module?: InjectorModule) { - } -} - -export function injectorReference(classTypeOrToken: T, module?: InjectorModule): any { - return new InjectorReference(classTypeOrToken, module); -} - export function tokenLabel(token: Token): string { if (token === null) return 'null'; if (token === undefined) return 'undefined'; @@ -62,6 +52,16 @@ export function tokenLabel(token: Token): string { return token + ''; } +function functionParameterNotFound(ofName: string, name: string, position: number, token: any) { + const argsCheck: string[] = []; + for (let i = 0; i < position; i++) argsCheck.push('✓'); + argsCheck.push('?'); + + throw new DependenciesUnmetError( + `Unknown function argument '${name}: ${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.` + ); +} + function constructorParameterNotFound(ofName: string, name: string, position: number, token: any) { const argsCheck: string[] = []; for (let i = 0; i < position; i++) argsCheck.push('✓'); @@ -122,54 +122,6 @@ function throwCircularDependency() { throw new CircularDependencyError(`Circular dependency found ${path}`); } -export type SetupProviderCalls = { - type: 'call', methodName: string | symbol | number, args: any[], order: number - } - | { type: 'property', property: string | symbol | number, value: any, order: number } - | { type: 'stop', order: number } - ; - -function matchType(left: Type, right: Type): boolean { - if (left.kind === ReflectionKind.class) { - if (right.kind === ReflectionKind.class) return left.classType === right.classType; - } - - return isExtendable(left, right); -} - -export class SetupProviderRegistry { - public calls: { type: Type, call: SetupProviderCalls }[] = []; - - public add(type: Type, call: SetupProviderCalls) { - this.calls.push({ type, call }); - } - - mergeInto(registry: SetupProviderRegistry): void { - for (const { type, call } of this.calls) { - registry.add(type, call); - } - } - - public get(token: Token): SetupProviderCalls[] { - const calls: SetupProviderCalls[] = []; - for (const call of this.calls) { - if (call.type === token) { - calls.push(call.call); - } else { - if (isClass(token)) { - const type = hasTypeInformation(token) ? reflect(token) : undefined; - if (type && matchType(type, call.type)) { - calls.push(call.call); - } - } else if (matchType(token, call.type)) { - calls.push(call.call); - } - } - } - return calls; - } -} - interface Scope { name: string; instances: { [name: string]: any }; @@ -320,6 +272,7 @@ export class Injector implements InjectorInterface { resolverCompiler.context.set('throwCircularDependency', throwCircularDependency); resolverCompiler.context.set('tokenNotfoundError', serviceNotfoundError); resolverCompiler.context.set('constructorParameterNotFound', constructorParameterNotFound); + resolverCompiler.context.set('functionParameterNotFound', functionParameterNotFound); resolverCompiler.context.set('propertyParameterNotFound', propertyParameterNotFound); resolverCompiler.context.set('factoryDependencyNotFound', factoryDependencyNotFound); resolverCompiler.context.set('transientInjectionTargetUnavailable', transientInjectionTargetUnavailable); @@ -461,11 +414,11 @@ export class Injector implements InjectorInterface { transient = provider.transient === true; const args: string[] = []; const reflection = ReflectionFunction.from(provider.useFactory); + const ofName = reflection.name === 'anonymous' ? 'useFactory' : reflection.name; 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, @@ -480,41 +433,35 @@ export class Injector implements InjectorInterface { const configureProvider: string[] = []; - const configuredProviderCalls = resolveDependenciesFrom[0].setupProviderRegistry?.get(token); - configuredProviderCalls.push(...buildContext.globalSetupProviderRegistry.get(token)); + const configurations = resolveDependenciesFrom[0].configurationProviderRegistry?.get(token); + configurations.push(...buildContext.globalConfigurationProviderRegistry.get(token)); - if (configuredProviderCalls) { - configuredProviderCalls.sort((a, b) => { - return a.order - b.order; + if (configurations?.length) { + configurations.sort((a, b) => { + return a.options.order - b.options.order; }); - for (const call of configuredProviderCalls) { - if (call.type === 'stop') break; - if (call.type === 'call') { - const args: string[] = []; - const methodName = 'symbol' === typeof call.methodName ? '[' + compiler.reserveVariable('arg', call.methodName) + ']' : call.methodName; - 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(getContainerToken(arg.to))}, scope, destination)`); - } else { - args.push(`${compiler.reserveVariable('arg', arg)}`); - } - } - - configureProvider.push(`${accessor}.${methodName}(${args.join(', ')});`); + for (const configure of configurations) { + const args: string[] = [accessor]; + const reflection = ReflectionFunction.from(configure.call); + const ofName = reflection.name === 'anonymous' ? 'configureProvider' : reflection.name; + + for (const parameter of reflection.getParameters().slice(1)) { + const tokenType = getInjectOptions(parameter.getType() as Type); + args.push(this.createFactoryProperty({ + name: parameter.name, + type: tokenType || parameter.getType() as Type, + optional: !parameter.isValueRequired() + }, provider, compiler, resolveDependenciesFrom, ofName, args.length, 'functionParameterNotFound')); } - if (call.type === 'property') { - const property = 'symbol' === typeof call.property ? '[' + compiler.reserveVariable('property', call.property) + ']' : call.property; - 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(getContainerToken(call.value.to))}, scope, destination)`; - } else { - value = compiler.reserveVariable('value', call.value); - } - configureProvider.push(`${accessor}.${property} = ${value};`); + + const call = `${compiler.reserveVariable('configure', configure.call)}(${args.join(', ')});`; + if (configure.options.replace) { + configureProvider.push(`${accessor} = ${call}`); + } else { + configureProvider.push(call); } + } } else { configureProvider.push('//no custom provider setup'); @@ -919,7 +866,7 @@ export class BuildContext { * In the process of preparing providers, each module redirects their * global setup calls in this registry. */ - globalSetupProviderRegistry: SetupProviderRegistry = new SetupProviderRegistry; + globalConfigurationProviderRegistry: ConfigurationProviderRegistry = new ConfigurationProviderRegistry; } export type Resolver = (scope?: Scope) => T; diff --git a/packages/injector/src/module.ts b/packages/injector/src/module.ts index e8cceaeff..c176a0025 100644 --- a/packages/injector/src/module.ts +++ b/packages/injector/src/module.ts @@ -1,6 +1,6 @@ import { NormalizedProvider, ProviderWithScope, TagProvider, Token } from './provider.js'; import { arrayRemoveItem, ClassType, getClassName, isClass, isPlainObject, isPrototypeOfBase } from '@deepkit/core'; -import { BuildContext, getContainerToken, Injector, resolveToken, SetupProviderRegistry } from './injector.js'; +import { BuildContext, getContainerToken, Injector, resolveToken } from './injector.js'; import { hasTypeInformation, isType, @@ -13,31 +13,59 @@ import { TypeClass, typeInfer, TypeObjectLiteral, - visit + visit, } from '@deepkit/type'; import { nominalCompatibility } from './types.js'; -export type ConfigureProvider = { [name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider : T[name] }; +export interface ConfigureProviderOptions { + /** + * If there are several registered configuration functions for the same token, + * they are executed in order of their `order` value ascending. The default is 0. + * The lower the number, the earlier it is executed. + */ + order: number; -/** - * Returns a configuration object that reflects the API of the given ClassType or token. Each call - * is scheduled and executed once the provider has been created by the dependency injection container. - */ -export function setupProvider(token: Token, registry: SetupProviderRegistry, order: number): ConfigureProvider ? C : T> { - const proxy = new Proxy({}, { - get(target, prop) { - return (...args: any[]) => { - registry.add(token, { type: 'call', methodName: prop, args: args, order }); - return proxy; - }; - }, - set(target, prop, value) { - registry.add(token, { type: 'property', property: prop, value: value, order }); - return true; + /** + * Replaces the instance with the value returned by the configuration function. + */ + replace: boolean; + + /** + * Per default only providers in the same module are configured. + * If you want to configure providers of all modules, set this to true. + */ + global: boolean; +} + +export interface ConfigureProviderEntry { + type: Type; + options: ConfigureProviderOptions; + call: Function; +} + +export class ConfigurationProviderRegistry { + public configurations: ConfigureProviderEntry[] = []; + + public add(type: Type, call: Function, options: ConfigureProviderOptions) { + this.configurations.push({ type, options, call }); + } + + mergeInto(registry: ConfigurationProviderRegistry): void { + for (const { type, options, call } of this.configurations) { + registry.add(type, call, options); } - }); + } - return proxy as any; + public get(token: Token): ConfigureProviderEntry[] { + const results: ConfigureProviderEntry[] = []; + for (const configure of this.configurations) { + const lookup = isClass(token) ? resolveReceiveType(token) : token; + if (dependencyLookupMatcher(configure.type, lookup)) { + results.push(configure); + } + } + return results; + } } let moduleIds: number = 0; @@ -251,8 +279,8 @@ export class InjectorModule(order: number = 0, classTypeOrToken?: ReceiveType): ConfigureProvider ? C : T> { - return setupProvider(resolveReceiveType(classTypeOrToken), this.setupProviderRegistry, order); - } - - /** - * Allows to register additional setup calls for a provider in the whole module tree. - * The injector token needs to be available in the local module providers. + * The purpose of a provider configuration is to configure the instance, for example + * call methods on it, set properties, etc. * - * Returns a object that reflects the API of the given ClassType or token. Each call - * is scheduled and executed once the provider is created by the dependency injection container. + * The first parameter of the function is always the instance of the provider that was created. + * All additional defined parameters will be provided by the dependency injection container. + * + * if `options.replace` is true, the returned value of `configure` will + * replace the instance. + * if `options.global` is true, the configuration function is applied to all + * providers in the whole module tree. + * The `options.order` defines the order of execution of the configuration function. + * The lower the number, the earlier it is executed. */ - setupGlobalProvider(order: number = 0, classTypeOrToken?: ReceiveType): ConfigureProvider ? C : T> { - return setupProvider(resolveReceiveType(classTypeOrToken), this.globalSetupProviderRegistry, order); + configureProvider(configure: (instance: T, ...args: any[]) => any, options: Partial = {}, type?: ReceiveType): this { + const optionsResolved = Object.assign({ order: 0, replace: false, global: false }, options); + type = resolveReceiveType(type); + const registry = options.global ? this.globalConfigurationProviderRegistry : this.configurationProviderRegistry; + registry.add(type, configure, optionsResolved); + return this; } getOrCreateInjector(buildContext: BuildContext): Injector { @@ -477,6 +509,11 @@ export class InjectorModule { { const module = new InjectorModule([MyService]); - module.setupProvider().addTransporter('a'); - module.setupProvider().addTransporter('b'); - expect(module.setupProviderRegistry.get(MyService).length).toBe(2); + module.configureProvider(v => v.addTransporter('a')); + module.configureProvider(v => v.addTransporter('b')); + expect(module.configurationProviderRegistry.get(MyService).length).toBe(2); const i1 = Injector.fromModule(module); expect(i1.get(MyService).transporter).toEqual(['a', 'b']); } { const module = new InjectorModule([MyService]); - module.setupProvider().transporter = ['a']; - module.setupProvider().transporter = ['a', 'b', 'c']; - expect(module.setupProviderRegistry.get(MyService).length).toBe(2); + module.configureProvider(v => v.transporter = ['a']); + module.configureProvider(v => v.transporter = ['a', 'b', 'c']); + expect(module.configurationProviderRegistry.get(MyService).length).toBe(2); const i1 = Injector.fromModule(module); expect(i1.get(MyService).transporter).toEqual(['a', 'b', 'c']); } diff --git a/packages/injector/tests/injector2.spec.ts b/packages/injector/tests/injector2.spec.ts index 1b31e2257..105ace51c 100644 --- a/packages/injector/tests/injector2.spec.ts +++ b/packages/injector/tests/injector2.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from '@jest/globals'; -import { InjectorContext, injectorReference } from '../src/injector.js'; +import { InjectorContext } from '../src/injector.js'; import { provide, Tag } from '../src/provider.js'; import { InjectorModule } from '../src/module.js'; import { InlineRuntimeType, ReflectionKind, Type, typeOf } from '@deepkit/type'; -import { Inject, InjectMeta, nominalCompatibility } from '../src/types.js'; +import { Inject, InjectMeta } from '../src/types.js'; test('basic', () => { class Service { @@ -221,7 +221,7 @@ test('exports have access to encapsulated module', () => { const root = new InjectorModule([Controller]); const module1 = new InjectorModule([ - Service, ServiceHelper + Service, ServiceHelper, ], root, {}, [Service]); const injector = new InjectorContext(root); @@ -251,7 +251,7 @@ test('exports have access to encapsulated module', () => { const module1 = new MiddleMan([], root, {}, []); const module2 = new ServiceModule([ - Service, ServiceHelper + Service, ServiceHelper, ], module1, {}, [Service]); module1.exports = [module2]; @@ -288,7 +288,7 @@ test('exports have access to encapsulated module', () => { const module2 = new MiddleMan2([], module1, {}, []); const module3 = new ServiceModule([ - Service, ServiceHelper + Service, ServiceHelper, ], module2, {}, [Service]); module1.exports = [module2]; @@ -317,7 +317,7 @@ test('useClass redirects and does not create 2 instances when its already provid { const root = new InjectorModule([ DefaultStrategy, - { provide: DefaultStrategy, useClass: SpecialisedStrategy } + { provide: DefaultStrategy, useClass: SpecialisedStrategy }, ]); const injector = new InjectorContext(root); @@ -331,7 +331,7 @@ test('useClass redirects and does not create 2 instances when its already provid { const root = new InjectorModule([ SpecialisedStrategy, - { provide: DefaultStrategy, useClass: SpecialisedStrategy } + { provide: DefaultStrategy, useClass: SpecialisedStrategy }, ]); const injector = new InjectorContext(root); @@ -345,7 +345,7 @@ test('useClass redirects and does not create 2 instances when its already provid { const root = new InjectorModule([ SpecialisedStrategy, - { provide: DefaultStrategy, useExisting: SpecialisedStrategy } + { provide: DefaultStrategy, useExisting: SpecialisedStrategy }, ]); const injector = new InjectorContext(root); @@ -407,7 +407,7 @@ test('forRoot', () => { ]); const module1 = new InjectorModule([ - Router + Router, ], root, {}, []).forRoot(); const injector = new InjectorContext(root); @@ -427,7 +427,7 @@ test('forRoot', () => { const module1 = new InjectorModule([], root, {}, []); const module2 = new InjectorModule([ - Router + Router, ], module1, {}, []).forRoot(); const injector = new InjectorContext(root); @@ -455,7 +455,7 @@ test('disableExports', () => { ]); const module1 = new InjectorModule([ - Router + Router, ], root, {}, []).disableExports(); const injector = new InjectorContext(root); @@ -505,7 +505,7 @@ test('non-exported dependencies can not be overwritten', () => { { const root = new InjectorModule([ - Controller + Controller, ]); const serviceModule = new InjectorModule([ @@ -526,7 +526,7 @@ test('non-exported dependencies can not be overwritten', () => { } const root = new InjectorModule([ - Controller, { provide: Service, useClass: MyService } + Controller, { provide: Service, useClass: MyService }, ]); const serviceModule = new InjectorModule([ @@ -585,7 +585,7 @@ test('forRoot module keeps reference to config', () => { const root = new InjectorModule([]); const module = new MyModule([ - Service + Service, ], root, { listen: 'localhost' }).forRoot().setConfigDefinition(Config); const injector = new InjectorContext(root); @@ -596,7 +596,7 @@ test('forRoot module keeps reference to config', () => { expect(service.listen).toBe('localhost'); }); -test('setup provider by class', () => { +test('configure provider by class', () => { class Service { list: any[] = []; @@ -605,10 +605,17 @@ test('setup provider by class', () => { } } - const root = new InjectorModule([Service]); + type ServiceB = any; + + const root = new InjectorModule([ + provide({ useValue: {} }), + Service, + ]); - root.setupProvider().add('a'); - root.setupProvider().add('b'); + root.configureProvider(service => { + service.add('a'); + service.add('b'); + }); const injector = new InjectorContext(root); const service = injector.get(Service); @@ -616,7 +623,7 @@ test('setup provider by class', () => { expect(service.list).toEqual(['a', 'b']); }); -test('setup provider by interface 1', () => { +test('setup matches only nominal', () => { class Service { list: any[] = []; @@ -625,14 +632,43 @@ test('setup provider by interface 1', () => { } } + type ServiceB = any; + + const val = {}; + const root = new InjectorModule([ + provide({ useValue: val }), + ]); + + // Service is not registered and although ServiceB is any, it should not match. + root.configureProvider(service => { + service.add('a'); + service.add('b'); + }); + + const injector = new InjectorContext(root); + const service = injector.get(); + expect(service === val).toBe(true); +}); + +test('configure provider by interface 1', () => { interface ServiceInterface { add(item: any): any; } + class Service implements ServiceInterface { + list: any[] = []; + + add(item: any) { + this.list.push(item); + } + } + const root = new InjectorModule([Service]); - root.setupProvider().add('a'); - root.setupProvider().add('b'); + root.configureProvider(service => { + service.add('a'); + service.add('b'); + }); const injector = new InjectorContext(root); const service = injector.get(Service); @@ -640,7 +676,7 @@ test('setup provider by interface 1', () => { expect(service.list).toEqual(['a', 'b']); }); -test('setup provider by interface 2', () => { +test('configure provider by interface 2', () => { class Service { list: any[] = []; @@ -657,8 +693,10 @@ test('setup provider by interface 2', () => { const root = new InjectorModule([provide(Service)]); - root.setupProvider().add('a'); - root.setupProvider().add('b'); + root.configureProvider(service => { + service.add('a'); + service.add('b'); + }); const injector = new InjectorContext(root); const service = injector.get(); @@ -666,7 +704,7 @@ test('setup provider by interface 2', () => { expect(service.list).toEqual(['a', 'b']); }); -test('setup provider in sub module', () => { +test('configure provider in sub module', () => { class Service { list: any[] = []; @@ -678,8 +716,10 @@ test('setup provider in sub module', () => { const root = new InjectorModule([]); const module = new InjectorModule([Service], root); - module.setupProvider().add('a'); - module.setupProvider().add('b'); + module.configureProvider(service => { + service.add('a'); + service.add('b'); + }); const injector = new InjectorContext(root); const service = injector.get(Service, module); @@ -687,7 +727,7 @@ test('setup provider in sub module', () => { expect(service.list).toEqual(['a', 'b']); }); -test('setup provider in exported sub module', () => { +test('configure provider in exported sub module', () => { class Service { list: any[] = []; @@ -699,8 +739,10 @@ test('setup provider in exported sub module', () => { const root = new InjectorModule([]); const module = new InjectorModule([Service], root, {}, [Service]); - module.setupProvider().add('a'); - module.setupProvider(0, Service).add('b'); + module.configureProvider(service => { + service.add('a'); + service.add('b'); + }); const injector = new InjectorContext(root); const service = injector.get(Service); @@ -708,7 +750,7 @@ test('setup provider in exported sub module', () => { expect(service.list).toEqual(['a', 'b']); }); -test('global setup provider', () => { +test('global configure provider', () => { class Service { list: any[] = []; @@ -721,8 +763,10 @@ test('global setup provider', () => { const module = new InjectorModule([], root); - module.setupGlobalProvider().add('a'); - module.setupGlobalProvider().add('b'); + module.configureProvider(service => { + service.add('a'); + service.add('b'); + }, { global: true }); const injector = new InjectorContext(root); const service = injector.get(Service); @@ -730,6 +774,91 @@ test('global setup provider', () => { expect(service.list).toEqual(['a', 'b']); }); +test('configure provider additional services', () => { + class Service { + } + + class Registry { + services: Service[] = []; + } + + const root = new InjectorModule([Registry, Service]); + root.configureProvider((service, registry: Registry) => { + registry.services.push(service); + }); + + const injector = new InjectorContext(root); + const service = injector.get(Service); + const registry = injector.get(Registry); + + expect(registry.services).toEqual([service]); +}); + +test('configure provider replace', () => { + class Service { + } + + class Replaced extends Service {} + + const root = new InjectorModule([Service]); + root.configureProvider(service => { + return new Replaced; + }, {replace: true}); + + const injector = new InjectorContext(root); + const service = injector.get(Service); + + expect(service).toBeInstanceOf(Replaced); +}) + +test('configure provider additional services scopes', () => { + class Service { + } + + class Registry { + services: Service[] = []; + } + + const root = new InjectorModule([Registry, { provide: Service, scope: 'http' }]); + root.configureProvider((service, registry: Registry) => { + registry.services.push(service); + }); + + const injector = new InjectorContext(root); + + const services: Service[] = []; + for (let i = 0; i < 10; i++) { + const scope = injector.createChildScope('http'); + const service = scope.get(Service); + services.push(service); + } + + const registry = injector.get(Registry); + expect(registry.services).toEqual(services); +}); + +test('configure provider additional services parent', () => { + class Service { + } + + class Registry { + services: Service[] = []; + } + + const root = new InjectorModule([Registry]); + const child = new InjectorModule([Service], root); + child.configureProvider((service, registry: Registry) => { + registry.services.push(service); + }); + + const injector = new InjectorContext(root); + + const service = injector.get(Service, child); + const registry = injector.get(Registry); + + expect(registry.services).toEqual([service]); +}); + test('second forRoot modules overwrites first forRoot providers', () => { class Service { } @@ -778,7 +907,7 @@ test('set service in scope', () => { const injector = InjectorContext.forProviders([ Service, { provide: Request, scope: 'tcp' }, - { provide: Connection, scope: 'tcp' } + { provide: Connection, scope: 'tcp' }, ]); const s1 = injector.get(Service); @@ -867,7 +996,7 @@ test('provide() with provider', () => { const root = new InjectorModule([ Service, - provide({ useValue: new RedisImplementation }) + provide({ useValue: new RedisImplementation }), ]); const injector = new InjectorContext(root); @@ -876,32 +1005,32 @@ test('provide() with provider', () => { expect(service.redis.get('abc')).toBe(true); }); -test('injectorReference from other module', () => { - class Service { - } - - class Registry { - services: any[] = []; - - register(service: any) { - this.services.push(service); - } - } - - const module1 = new InjectorModule([Service]); - const module2 = new InjectorModule([Registry]); - - module2.setupProvider().register(injectorReference(Service, module1)); - - const root = new InjectorModule([]).addImport(module1, module2); - const injector = new InjectorContext(root); - - { - const registry = injector.get(Registry, module2); - expect(registry.services).toHaveLength(1); - expect(registry.services[0]).toBeInstanceOf(Service); - } -}); +// test('injectorReference from other module', () => { +// class Service { +// } +// +// class Registry { +// services: any[] = []; +// +// register(service: any) { +// this.services.push(service); +// } +// } +// +// const module1 = new InjectorModule([Service]); +// const module2 = new InjectorModule([Registry]); +// +// module2.setupProvider().register(injectorReference(Service, module1)); +// +// const root = new InjectorModule([]).addImport(module1, module2); +// const injector = new InjectorContext(root); +// +// { +// const registry = injector.get(Registry, module2); +// expect(registry.services).toHaveLength(1); +// expect(registry.services[0]).toBeInstanceOf(Service); +// } +// }); test('inject its own module', () => { class Service { @@ -1038,7 +1167,7 @@ test('configuration work in deeply nested imports with overridden service', () = } const root = new InjectorModule([ - { provide: BrokerServer, useClass: BrokerMemoryServer } + { provide: BrokerServer, useClass: BrokerMemoryServer }, ]).addImport(frameworkModule); const injector = new InjectorContext(root); @@ -1291,9 +1420,9 @@ test('external pseudo class without annotation', () => { { provide: Stripe, useFactory() { return new Stripe; - } + }, }, - MyService + MyService, ]); const injector = new InjectorContext(app); @@ -1314,9 +1443,9 @@ test('external class without annotation', () => { { provide: Stripe, useFactory() { return new Stripe; - } + }, }, - MyService + MyService, ]); const injector = new InjectorContext(app); @@ -1341,14 +1470,14 @@ test('inject via string', () => { { provide: symbol1, useFactory() { return { value: 1 }; - } + }, }, { provide: symbol2, useFactory() { return { value: 2 }; - } + }, }, - MyService + MyService, ]); const injector = new InjectorContext(app); @@ -1374,14 +1503,14 @@ test('inject via symbols', () => { { provide: symbol1, useFactory() { return { value: 1 }; - } + }, }, { provide: symbol2, useFactory() { return { value: 2 }; - } + }, }, - MyService + MyService, ]); const injector = new InjectorContext(app); diff --git a/packages/injector/tsconfig.json b/packages/injector/tsconfig.json index a8259a593..0f84c5366 100644 --- a/packages/injector/tsconfig.json +++ b/packages/injector/tsconfig.json @@ -9,7 +9,7 @@ "emitDecoratorMetadata": true, "moduleResolution": "node", "preserveSymlinks": true, - "target": "es2018", + "target": "es2019", "module": "CommonJS", "esModuleInterop": true, "outDir": "./dist/cjs", diff --git a/packages/orm/src/database.ts b/packages/orm/src/database.ts index bf9fa10f1..57b8567f1 100644 --- a/packages/orm/src/database.ts +++ b/packages/orm/src/database.ts @@ -138,8 +138,8 @@ export class Database { public logger: DatabaseLogger = new DatabaseLogger(); /** @reflection never */ - public readonly eventDispatcher: EventDispatcher = new EventDispatcher(); - public readonly pluginRegistry: DatabasePluginRegistry = new DatabasePluginRegistry(); + public eventDispatcher: EventDispatcher = new EventDispatcher(); + public pluginRegistry: DatabasePluginRegistry = new DatabasePluginRegistry(); constructor( public readonly adapter: ADAPTER,