# @repo/core -- Clean Architecture Core Package This is the central business logic package. All domain entities, use cases, repository/service interfaces, controllers, and the InversifyJS DI container live here. Nothing in this package depends on any web framework (Next.js, TanStack, Payload). It is portable and testable in isolation. **Package location:** `packages/core` **Entry point:** `src/index.ts` **Test runner:** Vitest (`pnpm vitest run` from this directory) --- ## Layer Diagram Dependencies point inward (right to left). Outer layers depend on inner layers, never the reverse. ``` +------------------------------------------------------------------+ | di/ | | (wires everything together -- imports ALL internal layers) | +------------------------------------------------------------------+ | | | | v v v v +----------------+ +----------------+ +----------------+ +-----------+ | interface- | | infrastructure/| | application/ | | entities/ | | adapters/ | | (implements | | (use cases, | | (models, | | controllers/ | | interfaces | | repo/service | | errors) | | (validates | | with concrete | | interfaces) | | | | input, calls | | code) | | | | INNERMOST | | use cases) | | | | imports: | | zero deps | | | | imports: | | entities/ | | | | imports: | | application/ | | ONLY | | | | application/ | | entities/ | | | | | | entities/ | | @repo/ | | | | | | | | cms-client | | | | | +----------------+ +----------------+ +----------------+ +-----------+ ``` --- ## Import Rules | Layer | Can Import From | NEVER Import From | |---|---|---| | `entities/` | Nothing (Zod is the only external dependency) | `application/`, `infrastructure/`, `interface-adapters/`, `di/`, any `@repo/*`, any framework | | `application/` | `entities/` only | `infrastructure/`, `interface-adapters/`, `di/` (except `getInjection` from `di/container` in use cases) | | `interface-adapters/` | `application/` (use cases), `entities/` (types, errors) | `infrastructure/`, any `@repo/*` except via DI | | `infrastructure/` | `application/` (interfaces to implement), `entities/` (types), `@repo/cms-client` | `interface-adapters/`, apps/*, Next.js, TanStack | | `di/` | All internal layers (it wires them together) | apps/*, any framework package | Note: Use cases in `application/` import `getInjection` from `di/container` to resolve dependencies at runtime. This is the one controlled exception to the "application never imports di" rule -- `getInjection` is a lookup function, not a concrete implementation. --- ## DI Resolution Table | Symbol Key | Interface | Production Implementation | Mock Implementation | |---|---|---|---| | `IUsersRepository` | `IUsersRepository` (getUser, getUserByUsername, createUser) | PayloadUsersRepository (future -- will use `@repo/cms-client`) | `MockUsersRepository` (`infrastructure/repositories/mock-users.repository.ts`) | | `IArticlesRepository` | `IArticlesRepository` (getArticle, getArticles, createArticle, updateArticle) | PayloadArticlesRepository (future -- will use `@repo/cms-client`) | `MockArticlesRepository` (`infrastructure/repositories/mock-articles.repository.ts`) | | `IAuthenticationService` | `IAuthenticationService` (generateUserId, hashPassword, verifyPassword, validateSession, createSession, invalidateSession) | BetterAuthService (future) | `MockAuthenticationService` (`infrastructure/services/mock-auth.service.ts`) | | `ITelemetryService` | `ITelemetryService` (startSpan) | OTelSentryService (future) | `MockTelemetryService` (`infrastructure/services/mock-telemetry.service.ts`) | Currently all modules bind mock implementations. When production implementations are added, the modules will conditionally bind based on environment or configuration. --- ## Naming Conventions | Type | Pattern | Example | |---|---|---| | Entity model | `{name}.ts` | `article.ts`, `user.ts` | | Entity error | `{domain}.ts` | `auth.ts`, `common.ts` | | Repository interface | `{name}.repository.interface.ts` | `users.repository.interface.ts` | | Service interface | `{name}.service.interface.ts` | `auth.service.interface.ts` | | Use case | `{verb}-{noun}.use-case.ts` | `sign-in.use-case.ts`, `create-article.use-case.ts` | | Controller | `{noun}.controller.ts` (may export multiple functions) | `articles.controller.ts` | | Production impl | `{provider}-{name}.repository.ts` | `payload-users.repository.ts` | | Mock impl | `mock-{name}.repository.ts` or `mock-{name}.service.ts` | `mock-users.repository.ts` | | DI module | `{domain}.module.ts` | `auth.module.ts`, `content.module.ts` | | Test file | `{name}.test.ts` matching the source file name | `sign-in.use-case.test.ts` | --- ## How to Add a New Dependency (5-Step Recipe) This recipe adds a new repository or service interface to the DI container. For a complete feature, also follow the root `AGENTS.md` end-to-end recipe. ### Step 1: Define the interface in `application/` Create `src/application/repositories/{name}.repository.interface.ts` or `src/application/services/{name}.service.interface.ts`: ```typescript import type { MyEntity } from "@/entities/models/my-entity"; export interface IMyRepository { findById(id: string): Promise; findAll(): Promise; create(input: MyEntity): Promise; } ``` Export from the appropriate barrel: `src/application/repositories/index.ts` or `src/application/services/index.ts`. ### Step 2: Add the DI symbol to `di/types.ts` ```typescript import type { IMyRepository } from "@/application/repositories/my.repository.interface"; export const DI_SYMBOLS = { // ...existing symbols... IMyRepository: Symbol.for("IMyRepository"), }; export interface DI_RETURN_TYPES { // ...existing types... IMyRepository: IMyRepository; } ``` ### Step 3: Create mock implementation in `infrastructure/` ```typescript import { injectable } from "inversify"; import type { IMyRepository } from "@/application/repositories/my.repository.interface"; import type { MyEntity } from "@/entities/models/my-entity"; @injectable() export class MockMyRepository implements IMyRepository { private _items: MyEntity[] = []; async findById(id: string): Promise { return this._items.find((item) => item.id === id); } async findAll(): Promise { return [...this._items]; } async create(input: MyEntity): Promise { this._items.push(input); return input; } } ``` ### Step 4: Create or update DI module in `di/modules/` ```typescript 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(DI_SYMBOLS.IMyRepository).to(MockMyRepository); }; export const MyModule = new ContainerModule(initializeModule); ``` ### Step 5: Load module in `di/container.ts` ```typescript import { MyModule } from "./modules/my.module"; export const initializeContainer = () => { // ...existing modules... ApplicationContainer.load(MyModule); }; export const destroyContainer = () => { // ...existing modules... ApplicationContainer.unload(MyModule); }; ``` --- ## Testing Pattern All tests follow this lifecycle pattern: ```typescript import "reflect-metadata"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { destroyContainer, initializeContainer } from "@/di/container"; beforeEach(() => { initializeContainer(); }); afterEach(() => { destroyContainer(); }); describe("myUseCase", () => { it("does something", async () => { const result = await myUseCase(/* ... */); expect(result).toBeDefined(); }); }); ``` Critical requirements: - `import "reflect-metadata"` MUST be the first import in every test file. InversifyJS decorators rely on runtime metadata reflection. - `initializeContainer()` loads all DI modules (binding mock implementations). - `destroyContainer()` unloads all modules, ensuring clean state between tests. Without this, Singleton-scoped mocks retain state across tests. Test file locations mirror source structure: - `tests/unit/use-cases/{domain}/{name}.use-case.test.ts` - `tests/unit/controllers/{domain}/{name}.controller.test.ts` --- ## tsconfig Requirements These settings in `tsconfig.json` are MANDATORY. Do not remove them: | Setting | Why | |---|---| | `experimentalDecorators: true` | InversifyJS uses TypeScript decorators (`@injectable()`, `@inject()`) | | `emitDecoratorMetadata: true` | InversifyJS reads parameter type metadata at runtime for constructor injection | | `types: ["reflect-metadata", "node"]` | `reflect-metadata` polyfill must be globally available for decorator metadata | | Path alias `@/*` -> `./src/*` | All internal imports use `@/` prefix (e.g., `@/entities/models/user`) | The base config comes from `@repo/typescript-config/base.json` which already includes `experimentalDecorators` and `emitDecoratorMetadata`. The core `tsconfig.json` extends it and adds the path alias and types. --- ## Running Tests ```bash cd packages/core pnpm vitest run # Run all tests once pnpm vitest # Run in watch mode pnpm vitest run --reporter=verbose # Verbose output ``` --- ## Cross-References - `src/entities/AGENTS.md` -- Entity models and error classes - `src/application/AGENTS.md` -- Use cases, repository/service interfaces - `src/infrastructure/AGENTS.md` -- Concrete implementations with `@injectable()` - `src/interface-adapters/controllers/AGENTS.md` -- Controller patterns - `src/di/AGENTS.md` -- DI container configuration and lifecycle - `src/application/use-cases/auth/AGENTS.md` -- Auth domain business rules - `src/application/use-cases/content/AGENTS.md` -- Content domain business rules - Root `AGENTS.md` -- Full end-to-end feature recipe and monorepo map