docs: extend all 18 AGENTS.md files with comprehensive context, code examples, and recipes

This commit is contained in:
2026-04-06 15:31:03 +02:00
parent d06b900e7c
commit 0bc3b02f70
19 changed files with 4604 additions and 374 deletions

View File

@@ -1,37 +1,246 @@
# DI InversifyJS Container
# DI -- InversifyJS Dependency Injection Container
## Resolution Table
**Path:** `packages/core/src/di/`
**Role:** Wire together all abstract interfaces and their concrete implementations using InversifyJS. The container is the root composition point -- it knows about every layer and resolves dependencies at runtime. All other code accesses dependencies through `getInjection()`, never by importing implementations directly.
| Symbol Key | Interface | Production | Mock |
|---|---|---|---|
| IUsersRepository | IUsersRepository | (future) | MockUsersRepository |
| IArticlesRepository | IArticlesRepository | (future) | MockArticlesRepository |
| IAuthenticationService | IAuthenticationService | (future) | MockAuthenticationService |
| ITelemetryService | ITelemetryService | (future) | MockTelemetryService |
---
## How to Register a New Dependency
## Complete Resolution Table
1. Add Symbol to `types.ts``DI_SYMBOLS`
2. Add return type to `DI_RETURN_TYPES` interface
3. Create module in `modules/{domain}.module.ts`
4. Bind interface to implementation (production) and mock (test)
5. Load module in `container.ts` `initializeContainer()` and `destroyContainer()`
| Symbol Key | Interface | Production Implementation | Mock Implementation | DI Module |
|---|---|---|---|---|
| `IUsersRepository` | `IUsersRepository` (getUser, getUserByUsername, createUser) | PayloadUsersRepository (future) | `MockUsersRepository` (`infrastructure/repositories/mock-users.repository.ts`) | `auth.module.ts` |
| `IArticlesRepository` | `IArticlesRepository` (getArticle, getArticles, createArticle, updateArticle) | PayloadArticlesRepository (future) | `MockArticlesRepository` (`infrastructure/repositories/mock-articles.repository.ts`) | `content.module.ts` |
| `IAuthenticationService` | `IAuthenticationService` (generateUserId, hashPassword, verifyPassword, validateSession, createSession, invalidateSession) | BetterAuthService (future) | `MockAuthenticationService` (`infrastructure/services/mock-auth.service.ts`) | `auth.module.ts` |
| `ITelemetryService` | `ITelemetryService` (startSpan) | OTelSentryService (future) | `MockTelemetryService` (`infrastructure/services/mock-telemetry.service.ts`) | `auth.module.ts` |
---
## How to Register a New Dependency (Full Recipe)
### Step 1: Add symbol and return type to `types.ts`
```typescript
// di/types.ts
import type { IMyRepository } from "@/application/repositories/my.repository.interface";
export const DI_SYMBOLS = {
IAuthenticationService: Symbol.for("IAuthenticationService"),
ITelemetryService: Symbol.for("ITelemetryService"),
IUsersRepository: Symbol.for("IUsersRepository"),
IArticlesRepository: Symbol.for("IArticlesRepository"),
IMyRepository: Symbol.for("IMyRepository"), // <-- ADD THIS
};
export interface DI_RETURN_TYPES {
IAuthenticationService: IAuthenticationService;
ITelemetryService: ITelemetryService;
IUsersRepository: IUsersRepository;
IArticlesRepository: IArticlesRepository;
IMyRepository: IMyRepository; // <-- ADD THIS
}
```
The `DI_SYMBOLS` object maps string keys to unique `Symbol` values (used by InversifyJS for binding). The `DI_RETURN_TYPES` interface provides TypeScript type safety for `getInjection()`.
### Step 2: Create the DI module file
Create `di/modules/{domain}.module.ts` (or add to an existing one):
```typescript
// di/modules/my-domain.module.ts
import { ContainerModule, interfaces } from "inversify";
import type { IMyRepository } from "@/application/repositories/my.repository.interface";
import { MockMyRepository } from "@/infrastructure/repositories/mock-my.repository";
import { DI_SYMBOLS } from "../types";
const initializeModule = (bind: interfaces.Bind) => {
bind<IMyRepository>(DI_SYMBOLS.IMyRepository).to(MockMyRepository);
};
export const MyDomainModule = new ContainerModule(initializeModule);
```
For modules with multiple bindings (like `auth.module.ts`):
```typescript
const initializeModule = (bind: interfaces.Bind) => {
bind<IUsersRepository>(DI_SYMBOLS.IUsersRepository).to(MockUsersRepository);
bind<IAuthenticationService>(DI_SYMBOLS.IAuthenticationService).to(
MockAuthenticationService
);
};
```
### Step 3: Load and unload the module in `container.ts`
```typescript
// di/container.ts
import { MyDomainModule } from "./modules/my-domain.module";
export const initializeContainer = () => {
ApplicationContainer.load(AuthModule);
ApplicationContainer.load(ContentModule);
ApplicationContainer.load(MyDomainModule); // <-- ADD THIS
};
export const destroyContainer = () => {
ApplicationContainer.unload(AuthModule);
ApplicationContainer.unload(ContentModule);
ApplicationContainer.unload(MyDomainModule); // <-- ADD THIS
};
```
Both `load` and `unload` must be updated. Forgetting `unload` causes test isolation failures.
---
## Container Lifecycle
- Production: `initializeContainer()` runs automatically (not in test env)
- Tests: Call `initializeContainer()` in `beforeEach`, `destroyContainer()` in `afterEach`
- Test environments swap to mock implementations via module bindings
### Production (non-test environments)
```typescript
if (process.env.NODE_ENV !== "test") {
initializeContainer();
}
```
The container auto-initializes when the module is first imported. All bindings are available immediately.
### Test environments
Tests manually control the container lifecycle:
```typescript
import "reflect-metadata";
import { afterEach, beforeEach } from "vitest";
import { destroyContainer, initializeContainer } from "@/di/container";
beforeEach(() => {
initializeContainer(); // Load all modules, bind all implementations
});
afterEach(() => {
destroyContainer(); // Unload all modules, clear all bindings
});
```
Why manual control in tests:
- Each test gets a fresh container with fresh Singleton instances.
- In-memory mock data does not leak between tests.
- Forgetting `destroyContainer` in `afterEach` causes state pollution: the mock repositories retain data from previous tests, causing flaky tests.
---
## Scoping
The container defaults to **Singleton** scope:
```typescript
const ApplicationContainer = new Container({
defaultScope: "Singleton",
});
```
This means each call to `getInjection("IUsersRepository")` within a container lifecycle returns the same instance. This is correct for:
- Repositories (stateful mocks, connection pools in production)
- Services (auth sessions, telemetry clients)
When to use **Transient** scope (a new instance per resolution):
- Stateless utility services
- Per-request scoped objects
To override for a specific binding:
```typescript
bind<IMyService>(DI_SYMBOLS.IMyService)
.to(MyService)
.inTransientScope();
```
---
## `getInjection()` Usage in Use Cases
```typescript
// In a use case file:
import { getInjection } from "@/di/container";
export async function myUseCase(input: { id: string }) {
// The string key is type-safe: it must match a key in DI_SYMBOLS.
// The return type is automatically inferred from DI_RETURN_TYPES.
const usersRepository = getInjection("IUsersRepository");
// ^-- TypeScript infers: IUsersRepository
const authService = getInjection("IAuthenticationService");
// ^-- TypeScript infers: IAuthenticationService
const user = await usersRepository.getUser(input.id);
// ...
}
```
The `getInjection` function signature:
```typescript
export function getInjection<K extends keyof typeof DI_SYMBOLS>(
symbol: K
): DI_RETURN_TYPES[K] {
return ApplicationContainer.get(DI_SYMBOLS[symbol]);
}
```
This provides full type safety: if you pass `"IUsersRepository"`, the return type is `IUsersRepository`. If you pass an invalid key, TypeScript reports a compile error.
---
## DO NOT
- Import from apps/*
- Import framework-specific code (Next.js, TanStack, etc.)
- Use the container outside of this package — expose via `getInjection()` only
| Do Not | Why |
|---|---|
| Import from `apps/*` or framework packages (Next.js, TanStack) | DI is framework-agnostic. Framework code lives in apps. |
| Use the container outside of `@repo/core` | All external access goes through `getInjection()` or exported use case / controller functions. |
| Call `ApplicationContainer.get()` directly from use cases | Use `getInjection()` instead -- it provides type safety and a consistent API. |
| Forget to unload modules in `destroyContainer()` | Causes test state pollution (mock data leaks between tests). |
| Remove `import "reflect-metadata"` from `container.ts` | InversifyJS uses runtime reflection to read constructor parameter types. Without this import, `@inject()` decorators silently fail. |
## tsconfig Requirements (DO NOT REMOVE)
---
- `experimentalDecorators: true`
- `emitDecoratorMetadata: true`
- `types: ["reflect-metadata"]`
- `import "reflect-metadata"` at top of container.ts
## tsconfig Requirements
These settings are required for InversifyJS and MUST NOT be removed:
| Setting | Where | Why |
|---|---|---|
| `experimentalDecorators: true` | `@repo/typescript-config/base.json` | Enables `@injectable()` and `@inject()` decorator syntax used by InversifyJS |
| `emitDecoratorMetadata: true` | `@repo/typescript-config/base.json` | Emits runtime type metadata that InversifyJS reads to auto-resolve constructor parameter types |
| `types: ["reflect-metadata", "node"]` | `packages/core/tsconfig.json` | Makes `reflect-metadata` type definitions globally available. Required for `emitDecoratorMetadata` to function |
| `import "reflect-metadata"` | Top of `container.ts` and every test file | Polyfills the `Reflect.metadata` API at runtime. Without this, decorator metadata is not stored and `@inject()` silently injects `undefined` |
If any of these are removed, InversifyJS will throw errors like:
- `"No matching bindings found"` (metadata not emitted)
- `"Missing required @injectable annotation"` (decorators not enabled)
- `"Cannot read properties of undefined"` (reflect-metadata not imported)
---
## File Structure
```
di/
AGENTS.md
types.ts <-- DI_SYMBOLS + DI_RETURN_TYPES
container.ts <-- ApplicationContainer, initializeContainer, destroyContainer, getInjection
modules/
auth.module.ts <-- Binds IUsersRepository, IAuthenticationService, ITelemetryService
content.module.ts <-- Binds IArticlesRepository
```
---
## Cross-References
- `application/AGENTS.md` -- Defines the interfaces that are bound here
- `infrastructure/AGENTS.md` -- Provides the implementations that are bound here
- Root `AGENTS.md` -- Shows how DI fits into the full feature recipe