Skip to content

Commit

Permalink
feat(injector): new Module.configureProvider<T>(Fn) with configuratio…
Browse files Browse the repository at this point in the history
…n callback

Breaking Change.

This replaces the brittle Module.setupProvider API with something much more powerful.

The new `configureProvider` method on Module/App allows to register a function that is executed right after the provider was instantiated and allows to configure it arbitrarily:
Calling methods, set properties, or entirely replace the instance with something different (allow easier mocking for example).

```
const app = new App({
    providers: [Service]
});

// the callback is called once as soon as Service is constructed.
app.configureProvider<Service>(service => service.logging = true);

app.get<Service>();
```

The callback is handled by the DI container, so allows just like CLI functions, http routes, RPC actions, to defined arbitrary additional parameters that will be resolved and provided by the container.

```
// allows injecting config options
app.configureProvider<Service>((service, config: AppConfig) => {
    if (config.environment === 'dev') service.logging = true;
});

// or arbitrary additional stuff
app.configureProvider<Service>((service, ed: EventDispatcher, logger: Logger) => {
    service.eventDispatcher = ed;
    service.logger = ed;
});

// or replace it

app.configureProvider<Service>((service, ed: EventDispatcher, logger: Logger) => {
    return {};
}, { replace: true } );
```

The second options argument allows to define whether it replaces ({replace: true}) the service, whether it is applied to only services in the current module or all modules ({global: true}), and the order in which it is called ({order: 10}) in case multiple configuration functions are registered.
  • Loading branch information
marcj committed Jan 31, 2024
1 parent 47cccf1 commit 1739b95
Show file tree
Hide file tree
Showing 11 changed files with 371 additions and 259 deletions.
11 changes: 6 additions & 5 deletions packages/app/tests/module.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -151,6 +151,7 @@ test('configured provider', () => {

addTransport(transport: any) {
this.transporter.push(transport);
return this;
}
}

Expand All @@ -164,31 +165,31 @@ test('configured provider', () => {
{
const module = new AppModule();
const logger = new ServiceContainer(module.setup((module) => {
module.setupProvider<Logger>().addTransport('first').addTransport('second');
module.configureProvider<Logger>(v => v.addTransport('first').addTransport('second'));
})).getInjector(module).get(Logger);
expect(logger.transporter).toEqual(['first', 'second']);
}

{
const module = new AppModule();
const logger = new ServiceContainer(module.setup((module) => {
module.setupProvider<Logger>().transporter = ['first', 'second', 'third'];
module.configureProvider<Logger>(v => v.transporter = ['first', 'second', 'third']);
})).getInjector(module).get(Logger);
expect(logger.transporter).toEqual(['first', 'second', 'third']);
}

{
const module = new AppModule();
const logger = new ServiceContainer(module.setup((module) => {
module.setupProvider<Logger>().addTransport(new Transporter);
module.configureProvider<Logger>(v => v.addTransport(new Transporter));
})).getInjector(module).get(Logger);
expect(logger.transporter[0] instanceof Transporter).toBe(true);
}

{
const module = new AppModule();
const logger = new ServiceContainer(module.setup((module) => {
module.setupProvider<Logger>().addTransport(injectorReference(Transporter));
module.configureProvider<Logger>((v, t: Transporter) => v.addTransport(t));
})).getInjector(module).get(Logger);
expect(logger.transporter[0] instanceof Transporter).toBe(true);
}
Expand Down
9 changes: 5 additions & 4 deletions packages/example-app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Logger>().setTransport([new JSONTransport]);
}
module.configureProvider<Logger>(logger => {
if (config.environment === 'production') {
logger.setTransport([new JSONTransport]);
}
});
});

app.loadConfigFromEnv().run();
44 changes: 21 additions & 23 deletions packages/framework/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -164,8 +164,8 @@ export class FrameworkModule extends createModule({
this.addImport();
this.addProvider({ provide: RpcControllers, useValue: this.rpcControllers });

this.setupProvider<MigrationProvider>().setMigrationDir(this.config.migrationDir);
this.setupProvider<DatabaseRegistry>().setMigrateOnStartup(this.config.migrateOnStartup);
this.configureProvider<MigrationProvider>(v => v.setMigrationDir(this.config.migrationDir));
this.configureProvider<DatabaseRegistry>(v => v.setMigrateOnStartup(this.config.migrateOnStartup));

if (this.config.httpLog) {
this.addListener(HttpLogger);
Expand Down Expand Up @@ -217,35 +217,33 @@ export class FrameworkModule extends createModule({
this.addProvider(DebugBrokerBus);
this.addProvider({ provide: StopwatchStore, useClass: FileStopwatchStore });

const stopwatch = this.setupGlobalProvider<Stopwatch>();
if (this.config.profile || this.config.debug) {
stopwatch.enable();
} else {
stopwatch.disable();
}

const stopwatch = this.configureProvider<Stopwatch>(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<FilesystemRegistry>().addFilesystem(fs.classType, fs.module);
}
this.configureProvider<FilesystemRegistry>(v => {
for (const fs of this.filesystems) {
v.addFilesystem(fs.classType, fs.module);
}
});
}

protected setupDatabase() {
for (const db of this.dbs) {
this.setupProvider<DatabaseRegistry>().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<DatabaseRegistry>(v => v.addDatabase(db.classType, {}, db.module));
db.module.configureProvider((db: Database, eventDispatcher: EventDispatcher, stopwatch: Stopwatch) => {
db.eventDispatcher = eventDispatcher;
db.stopwatch = stopwatch;
}, {}, db.classType);
}
}

Expand Down
7 changes: 3 additions & 4 deletions packages/framework/src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -77,8 +76,8 @@ export class BrokerMemoryServer extends BrokerServer {
export function createTestingApp<O extends RootModuleDefinition>(options: O, entities: ClassType[] = [], setup?: (module: AppModule<any>) => void): TestingFacade<App<O>> {
const module = new RootAppModule(options);

module.setupGlobalProvider<Logger>().removeTransport(injectorReference(ConsoleTransport));
module.setupGlobalProvider<Logger>().addTransport(injectorReference(MemoryLoggerTransport));
module.configureProvider<Logger>((v, t: ConsoleTransport) => v.removeTransport(t));
module.configureProvider<Logger>((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
Expand All @@ -100,7 +99,7 @@ export function createTestingApp<O extends RootModuleDefinition>(options: O, ent

if (entities.length) {
module.addProvider({ provide: Database, useValue: new Database(new MemoryDatabaseAdapter, entities) });
module.setupGlobalProvider<DatabaseRegistry>().addDatabase(Database, {}, module);
module.configureProvider<DatabaseRegistry>(v => v.addDatabase(Database, {}, module));
}

if (setup) module.setup(setup as any);
Expand Down
14 changes: 7 additions & 7 deletions packages/http/src/static-serving.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>, path: string, localPath: string = path): ClassType {
class HttpRequestStaticServingListener {
Expand Down Expand Up @@ -48,9 +48,9 @@ export function serveStaticListener(module: AppModule<any>, 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);
Expand Down Expand Up @@ -132,20 +132,20 @@ export function registerStaticHttpController(module: AppModule<any>, options: St
type: 'controller',
controller: StaticController,
module,
methodName: 'serveIndex'
methodName: 'serveIndex',
});
route1.groups = groups;
module.setupGlobalProvider<HttpRouter>().addRoute(route1);
module.configureProvider<HttpRouter>(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<HttpRouter>().addRoute(route2);
module.configureProvider<HttpRouter>(router => router.addRoute(route2), { global: true });
}

module.addProvider(StaticController);
Expand Down
Loading

0 comments on commit 1739b95

Please sign in to comment.