docs: extend all 18 AGENTS.md files with comprehensive context, code examples, and recipes
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user