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
آخر نشاط في المصدر
٢٤ سبتمبر ٢٠٢٦ في ١٦:٣٤
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١
التفرعات
٠

خيارات التثبيت

يُحدَّد 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