Skip to main content

dependency-injection

Use when creating abstractions, implementations, features, repositories, use cases, or any DI-wired service. Defines mandatory file separation — abstractions, implementations, null objects, and features MUST each be in their own file. Invoke BEFORE writing any DI-related code.

소스 정보

저장소
webiny/data-mock
최근 소스 활동
2026년 9월 24일 16:34
감지된 SKILL.md 언어
영어
스타
1
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
dependency-injection
description
Use when creating abstractions, implementations, features, repositories, use cases, or any DI-wired service. Defines mandatory file separation — abstractions, implementations, null objects, and features MUST each be in their own file. Invoke BEFORE writing any DI-related code.
# Dependency Injection — @webiny/di Conventions Every service lives behind an abstraction token. Implementations are classes bound to an abstraction. Consumers resolve the abstraction, never the implementation. Each concern gets its own file. ## The Iron Rule **Every DI concern lives in its own file. No exceptions.** | Concern | File | Directory | | --------------------------------------------- | ------------------------------- | ------------------------------------- | | Abstraction (interface + token + namespace) | `abstractions/XxxRepository.ts` | `abstractions/` | | Implementation (class + createImplementation) | `XxxRepository.ts` | feature root | | Null/default implementation | `NullXxxContext.ts` | alongside abstraction or feature root | | Feature (createFeature + registrations) | `feature.ts` or `XxxFeature.ts` | feature root | Never combine these in one file. A file with `createAbstraction()` must not contain `createImplementation()`, a class, or `createFeature()`. ## Naming Conventions ### Class Names vs Export Names The `Impl` suffix exists **only on the class declaration**, never on anything exported or imported. ```ts // INSIDE the implementation file: class ProjectRepositoryImpl implements Abstraction.Interface { ... } // Impl on class — OK export const ProjectRepository = Abstraction.createImplementation({ // NO Impl on const implementation: ProjectRepositoryImpl, dependencies: [DatabaseClient] }); ``` When the same short name (`ProjectRepository`) is used by both the abstraction token and the implementation const, they live in different files — the import path distinguishes them. The `as Abstraction` alias inside the implementation file resolves the local clash: ```ts import { ProjectRepository as Abstraction } from "./abstractions/ProjectRepository.js"; ``` ### General Naming Rules - Never abbreviate: `projectRepository` not `projRepo`, `authorizationService` not `authSvc` - Never use shorthand: `Project` not `Proj` - Constructor deps are always `private readonly` with full names - All class methods and properties MUST have explicit access modifiers (`public`, `private`, `protected`) or use JS private `#` fields — implicit public is forbidden ## Abstraction File **Abstractions MUST live in an `abstractions/` directory — never a flat `abstractions.ts` file.** Each abstraction gets its own file inside `abstractions/`, plus a barrel `abstractions/index.ts` that re-exports all tokens and interfaces. One file per abstraction, one `createAbstraction()` call per file. Each file contains: interface, `createAbstraction()` call, namespace with type exports. **Nothing else — no classes, no implementations, no features.** **Every abstraction MUST have a sibling namespace with exported types. No exceptions.** At minimum the namespace exports `Interface` (the abstraction's own interface). If the abstraction consumes external types, those are re-exported through the namespace too. An abstraction without a namespace is incomplete. ```ts // abstractions/ProjectRepository.ts import { createAbstraction } from "@webiny/stdlib"; import type { Result } from "@webiny/stdlib"; import type { ProjectNotFoundError, ProjectPersistenceError } from "../errors.js"; export interface ProjectRecord { id: string; name: string; apiUrl: string; apiToken: string; tenant: string; createdAt: number; updatedAt: number; } interface ProjectCreateInput { name: string; apiUrl: string; apiToken: string; tenant?: string; } export interface IProjectRepository { list(): Promise<Result<ProjectRecord[], ProjectPersistenceError>>; getById(id: string): Promise<Result<ProjectRecord, ProjectNotFoundError | ProjectPersistenceError>>; create(input: ProjectCreateInput): Promise<Result<ProjectRecord, ProjectPersistenceError>>; remove(id: string): Promise<Result<void, ProjectNotFoundError | ProjectPersistenceError>>; } export const ProjectRepository = createAbstraction<IProjectRepository>("Shared/ProjectRepository"); export namespace ProjectRepository { export type Interface = IProjectRepository; export type Record = ProjectRecord; export type CreateInput = ProjectCreateInput; export type Error = ProjectNotFoundError | ProjectPersistenceError; } ``` **Rules:** - Interface is `export interface` (required for strict declaration emit) - All types accessed via namespace only (`ProjectRepository.Interface`, `ProjectRepository.Record`) - Every param type gets its own named interface — never inline structural types - No `Parameters<>`, `ReturnType<>`, or indexed access types — export explicit named types - The namespace must contain ALL types — no top-level type exports scattered outside it - **The namespace re-exports every type the implementation needs** — implementations import only the abstraction alias and reference types as `Abstraction.Record`, `Abstraction.CreateInput`, etc. ## Implementation File Separate file at the feature root. Uses a local rename alias (`as Abstraction`) to avoid name clash. ```ts // ProjectRepository.ts (at feature root, NOT in abstractions/) import { Result } from "@webiny/stdlib"; import { DatabaseClient } from "~/db/abstractions/DatabaseClient.js"; import { ProjectRepository as Abstraction } from "./abstractions/ProjectRepository.js"; import { projects } from "~/db/schema.js"; import { eq } from "drizzle-orm"; import { generateId } from "@webiny/stdlib"; import { ProjectNotFoundError, ProjectPersistenceError } from "./errors.js"; class ProjectRepositoryImpl implements Abstraction.Interface { public constructor( private readonly databaseClient: DatabaseClient.Interface ) {} public async list(): Promise<Result<Abstraction.Record[], ProjectPersistenceError>> { try { const rows = this.databaseClient.db.select().from(projects).all(); return Result.ok(rows); } catch (error) { return Result.fail(new ProjectPersistenceError(error as Error)); } } // ... other methods } export const ProjectRepository = Abstraction.createImplementation({ implementation: ProjectRepositoryImpl, dependencies: [DatabaseClient] }); ``` **Key points:** - Uses `Abstraction.createImplementation()` — the abstraction token's own method - `dependencies` array is positional — matches constructor parameters in order - Class is NOT exported — only the createImplementation const is - Uses `Abstraction.Record`, `Abstraction.CreateInput` — never imports types from other files directly ## Null/Default Implementations When a service needs a fallback, the null object gets **its own file**. ```ts // NullGraphQLClient.ts (own file, NOT in the abstraction file) import { GraphQLClient } from "./abstractions/GraphQLClient.js"; class NullGraphQLClientImpl implements GraphQLClient.Interface { // ... stub methods } export const NullGraphQLClient = NullGraphQLClientImpl; ``` **Never** put a class in an abstraction file. ## Feature File Container registrations live in `feature.ts`. Imports the implementation (not the abstraction) for registration. The `register()` function must be **synchronous**. ```ts // feature.ts import { createFeature } from "@webiny/stdlib"; import { ProjectRepository } from "./ProjectRepository.js"; // the createImplementation export export const ProjectsFeature = createFeature({ name: "Shared/ProjectsFeature", register(container) { container.register(ProjectRepository).inSingletonScope(); } }); ``` Features compose — a parent feature calls child features in its `register`: ```ts register(container) { DatabaseFeature.register(container, { databaseClient }); CacheFeature.register(container, { cacheDir }); GeneratorFeature.register(container); } ``` ## Barrel Exports (index.ts) Export **abstractions only** — tokens and their types. Never a feature, never an implementation. ```ts // index.ts export { ProjectRepository } from "./abstractions/index.js"; // abstraction token // NEVER: export { ProjectsFeature } from "./feature.js" // feature // NEVER: export { ProjectRepository } from "./ProjectRepository.js" // implementation ``` A feature is registered once, and whoever registers it imports it from that domain's own `feature.ts`. Routing it through a barrel gives a second way in, and the two drift. **An implementation is never imported outside its own domain directory.** In practice its only importer is that domain's `feature.ts`. Everything else — presenters, use cases, other features, tests — depends on the abstraction and lets the container supply the implementation. An implementation reached directly is one the container cannot substitute, which is the whole point of having it. ## Lifetime Scopes ```ts container.register(Impl); // Transient — new instance per resolve container.register(Impl).inSingletonScope(); // Singleton — one instance per container container.registerInstance(Abstraction, value); // Pre-built instance (always singleton) container.registerFactory(Abstraction, () => v); // Lazy factory ``` **House rule:** Singletons for stateless services (repositories, clients, gateways, loggers). Transient for per-request state (use cases, presenters used standalone). Presenters injected as DI deps of other presenters must be singleton. ## Dependencies The `dependencies` array is positional — matches constructor parameters in order: ```ts export const ProjectRepository = Abstraction.createImplementation({ implementation: ProjectRepositoryImpl, dependencies: [DatabaseClient] // matches constructor(client: DatabaseClient.Interface) }); ``` Options: `[Dep, { optional: true }]` for optional, `[Dep, { multiple: true }]` for resolveAll. Every constructor dep is `private readonly` with the full type from the abstraction namespace: ```ts public constructor( private readonly projectRepository: ProjectRepository.Interface, private readonly graphqlClient: GraphQLClient.Interface ) {} ``` ## Testing Use `createTestContainer()` for all tests. It creates a fully-wired container with real SQLite, real generators, and a mock HttpClient (for external API calls). Resolve via container, never construct directly: ```ts const tc = createTestContainer({ httpClient: mockHttpClient }); const repository = tc.container.resolve(ProjectRepository); // or for API tests: const app = await createServer(tc.container, [registerApiRoutes]); const response = await app.inject({ method: "GET", url: "/api/projects" }); ``` **Rules:** - Never `new ProjectRepositoryImpl(dep)` — always resolve through DI - Only external HTTP calls may be mocked (via HttpClient abstraction) - Build a fresh container per test to avoid singleton state bleeding - Use `tc.cleanup()` in afterEach to remove temp DB files ## Common Mistakes | Mistake | Fix | | ------------------------------------------------ | ---------------------------------------------------------------- | | Class in abstraction file | Extract to own file (`NullXxx.ts` or `Xxx.ts`) | | Feature + impl in same file | Split into `feature.ts` and `Xxx.ts` | | Multiple concerns in one file | One responsibility per file — split immediately | | `Impl` suffix on exported const | Only the class gets `Impl`; const uses short name | | Exporting implementation from index.ts | Only export abstractions and features from barrels | | Bare interface export (`export interface IFoo`) | Access via namespace: `Foo.Interface` | | Inline structural types in signatures | Extract to named interface/type in namespace | | Flat `abstractions.ts` file | Always use `abstractions/` directory with one file per token | | Multiple `createAbstraction()` in one file | Split into separate files inside `abstractions/` | | Abstraction without sibling namespace | Every abstraction MUST have a namespace (at minimum `Interface`) | | `vi.fn()` mocks for repos/services | Use real implementations via test container | | `new ImplClass(dep)` in tests | Resolve through DI container | | Async `register()` in features | `register()` is sync; async work in startup functions | | `registerInstance()` for code that should use `createImplementation()` | Use `createImplementation()` with `dependencies` array when possible |
GitHub에서 보기