Skip to content
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
13 changes: 13 additions & 0 deletions docs/reference/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ The generated container follows the project convention: one default-exported
class per file. Application classes should receive dependencies through their
constructor.

## Service Lookup

Use `getService()` when a service must exist:

```ts
const repository = kernel.di.getService(UserRepository);
```

Use `hasService()` when an adapter needs to check whether a token is registered
without resolving it. This does not call the underlying container `get()` method,
so missing services can be detected without emitting
`node-dependency-injection` “service is not registered” warnings.

## Overrides

Use dependency overrides when the application wants a different implementation
Expand Down
20 changes: 20 additions & 0 deletions docs/reference/express-kernel-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ const server = new ExpressKernelServer({
`controllers` are merged with `kernel.getRoutes()` before
`routing-controllers` is configured.

Controllers passed through `controllers`, `registerControllers()` or
`kernel.registerRoutes()` are known to the Express adapter. When
`routing-controllers` asks for one of those classes, `ExpressKernelServer` first
checks the kernel container without resolving the service. If the class itself is
not registered, the adapter creates and caches a plain controller instance
instead of forcing every external controller to be a DI service.

That avoids the “service is not registered” warning that
`node-dependency-injection` emits when a missing service is resolved directly.
Registered controllers still resolve through DI. Dependency errors raised while
resolving a registered controller are rethrown, so missing constructor
dependencies, broken factories and other container failures remain visible.

Custom kernels or test doubles should expose `di.hasService()` when they need
registered controllers to resolve through DI. Without that optional lookup,
known controllers are treated as external controllers and constructed locally.
Classes requested by `routing-controllers` that are not known controllers, such
as plain middleware or interceptor classes, keep the normal `routing-controllers`
fallback when they are not registered in DI.

The same can be done after construction:

```ts
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@
"lint": "eslint ./src --ext .ts",
"lint:fix": "eslint ./src --ext .ts --fix",
"prepack": "yarn build",
"test": "yarn build && c8 node --test \"tests/**/*.test.mjs\"",
"test": "yarn build && c8 node --test --test-concurrency=1 \"tests/**/*.test.mjs\"",
"build:coverage": "rm -rf dist && tsc -p tsconfig.coverage.json",
"test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/KernelDefaultEnvironment.ts\" --exclude \"src/kernel/KernelEnvironment.ts\" --exclude \"src/kernel/KernelEnvironmentForSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchemaInput.ts\" --exclude \"src/kernel/KernelEnvironmentValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariable.ts\" --exclude \"src/kernel/KernelEnvironmentVariablePrimitive.ts\" --exclude \"src/kernel/KernelEnvironmentVariableResolvedValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariableType.ts\" --exclude \"src/kernel/ShutdownCandidate.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"",
"test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/KernelDefaultEnvironment.ts\" --exclude \"src/kernel/KernelEnvironment.ts\" --exclude \"src/kernel/KernelEnvironmentForSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchemaInput.ts\" --exclude \"src/kernel/KernelEnvironmentValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariable.ts\" --exclude \"src/kernel/KernelEnvironmentVariablePrimitive.ts\" --exclude \"src/kernel/KernelEnvironmentVariableResolvedValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariableType.ts\" --exclude \"src/kernel/ShutdownCandidate.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test --test-concurrency=1 \"tests/**/*.test.mjs\"",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [
Expand Down
59 changes: 59 additions & 0 deletions src/adapters/ui/express/ExpressControllerResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ExpressController } from './ExpressController.js';
import type { ExpressKernelServerOptions } from './ExpressKernelServerOptions.js';

export class ExpressControllerResolver {
private readonly controllerInstances = new Map<ExpressController, unknown>();

private readonly controllers: Set<ExpressController>;

constructor(
private readonly kernel: ExpressKernelServerOptions['kernel'],
controllers: readonly ExpressController[],
) {
this.controllers = new Set(controllers);
}

private createControllerInstance(
ClassDefinition: ExpressController,
): unknown {
const Controller = ClassDefinition as new () => unknown;

return new Controller();
}

private getCachedController(ClassDefinition: ExpressController): unknown {
const cached = this.controllerInstances.get(ClassDefinition);

if (cached) {
return cached;
}

const instance = this.createControllerInstance(ClassDefinition);

this.controllerInstances.set(ClassDefinition, instance);

return instance;
}

private isKnownController(ClassDefinition: ExpressController): boolean {
return this.controllers.has(ClassDefinition);
}

private canResolveService(ClassDefinition: ExpressController): boolean {
return this.kernel.di.hasService?.(ClassDefinition) === true;
}

public get(ClassDefinition: ExpressController): unknown {
if (!this.canResolveService(ClassDefinition)) {
if (this.isKnownController(ClassDefinition)) {
return this.getCachedController(ClassDefinition);
}

return undefined;
}

return this.kernel.di.getService(ClassDefinition);
}
}

export default ExpressControllerResolver;
30 changes: 21 additions & 9 deletions src/adapters/ui/express/ExpressKernelServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type { ExpressPhaseHook } from './ExpressPhaseHook.js';
import type { HttpApp } from './HttpApp.js';
import type { HttpServer } from './HttpServer.js';

import { ExpressControllerResolver } from './ExpressControllerResolver.js';

export class ExpressKernelServer {
private readonly applicationRequire = createRequire(
path.resolve(process.cwd(), 'package.json'),
Expand Down Expand Up @@ -58,18 +60,23 @@ export class ExpressKernelServer {
return [...(items ?? [])];
}

private configureControllerContainer(): void {
private configureControllerContainer(
controllers: readonly ExpressController[],
): void {
const { useContainer } = this.getRoutingControllers();
const resolver = new ExpressControllerResolver(
this.options.kernel,
controllers,
);

useContainer(
{
/* c8 ignore next */
get: (ClassDefinition: ExpressController) =>
this.options.kernel.di.getService(ClassDefinition),
get: <T>(ClassDefinition: new (...args: never[]) => T): T =>
resolver.get(ClassDefinition as unknown as ExpressController) as T,
},
{
fallback: true,
fallbackOnErrors: true,
fallbackOnErrors: false,
Comment thread
haskou marked this conversation as resolved.
},
);
}
Expand Down Expand Up @@ -101,9 +108,14 @@ export class ExpressKernelServer {

private defaultErrorHandler(): ErrorRequestHandler {
return (error, request, response, next) => {
void next;
void request;

if (response.headersSent) {
next(error);

return;
}

response.status(500).json({
error: error instanceof Error ? error.message : String(error),
});
Expand Down Expand Up @@ -250,10 +262,10 @@ export class ExpressKernelServer {
throw new Error('HTTP server is already running.');
}

const controllers = [
const controllers: ExpressController[] = [
...this.options.kernel.getRoutes(),
...this.controllers,
];
] as ExpressController[];
const express = this.getExpress();
const { useExpressServer } = this.getRoutingControllers();
const app = express() as HttpApp;
Expand All @@ -262,7 +274,7 @@ export class ExpressKernelServer {
this.applyMiddlewares(app, this.preControllerMiddlewares);
await this.runHooks(this.beforeControllersHooks, app);
await this.runPhaseHooks('beforeControllers', app);
this.configureControllerContainer();
this.configureControllerContainer(controllers);
useExpressServer(app, {
...this.options.routingControllersOptions,
controllers,
Expand Down
1 change: 1 addition & 0 deletions src/adapters/ui/express/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './ErrorExplanation.js';
export * from './ErrorResponseHandler.js';
export * from './ExpressAppHook.js';
export * from './ExpressController.js';
export * from './ExpressControllerResolver.js';
export * from './ExpressHookPhase.js';
export * from './ExpressKernelServer.js';
export * from './ExpressKernelServerOptions.js';
Expand Down
1 change: 1 addition & 0 deletions src/contracts/kernel/ServiceResolver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export interface ServiceResolver {
getService<T>(serviceName: unknown): T;
hasService?(serviceName: unknown): boolean;
}
17 changes: 17 additions & 0 deletions src/infrastructure/dependency-injection/DependencyInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ export class DependencyInjection implements ServiceResolver {
return `ddd-kernel.override.${prefix}.${tokenName}`;
}

private hasDirectService(serviceName: unknown): boolean {
return (
typeof serviceName === 'string' &&
(this.definitions.has(serviceName) || this.aliases.has(serviceName))
);
}

private ensureSyntheticService(id: string, value: unknown): void {
const definition = this.container.register(id);

Expand Down Expand Up @@ -365,6 +372,16 @@ export class DependencyInjection implements ServiceResolver {

return this.container.get<T>(serviceName);
}

public hasService(serviceName: unknown): boolean {
return (
this.overrideTokenIds.has(serviceName) ||
this.findAliasServiceId(serviceName) !== undefined ||
this.findConcreteChildServiceId(serviceName) !== undefined ||
this.findRegisteredServiceId(serviceName) !== undefined ||
this.hasDirectService(serviceName)
);
}
}

export default DependencyInjection;
Loading