repositories
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRepositories
仓库
This skill documents how to structure Repository Interfaces in the Domain and Concrete Implementations in Infrastructure using our tightly coupled stack: , , , and Railway-Oriented Programming ().
drizzle-ormnode-postgresdrizzle-kitneverthrowDEPENDENCY NOTICE: This skill works in tandem with the skill. Review both when managing vertical slices of domain logic.
use-cases本技能文档介绍如何使用我们紧密耦合的技术栈:、、和面向铁路编程(),在领域层构建仓库接口,在基础设施层实现具体实例。
drizzle-ormnode-postgresdrizzle-kitneverthrow依赖说明:本技能需与技能配合使用。在管理领域逻辑垂直切片时,请同时参考这两项技能。
use-cases1. RFC 2119 Language
1. RFC 2119术语规范
This skill uses RFC 2119 keywords for precise requirement specification:
- MUST / MUST NOT: Mandatory requirements. Violations break correctness or security.
- SHOULD / SHOULD NOT: Strong recommendations. Only deviate with documented justification.
本技能使用RFC 2119关键词来明确需求规格:
- MUST / MUST NOT:强制性要求。违反会破坏正确性或安全性。
- SHOULD / SHOULD NOT:强烈建议。只有在有文档记录的理由时才可偏离。
2. File Location & Naming
2. 文件位置与命名规范
Interfaces
- Location:
src/<context>/domain/interfaces/ - Naming: File named containing the inner
entityRepository.tsclass.EntityRepository
Implementations
- Location:
src/<context>/infrastructure/ - Naming:
postgresEntityRepository.ts
接口
- 位置:
src/<context>/domain/interfaces/ - 命名:文件名为,内部包含
entityRepository.ts类。EntityRepository
实现类
- 位置:
src/<context>/infrastructure/ - 命名:
postgresEntityRepository.ts
3. Defining the Domain Interface
3. 定义领域接口
Reference:
references/examples/repository-interface-example.md- MUST: Be defined as an , NOT an
abstract class. This is required so the class symbol can be used as an injection token by our service containers.interface - MUST: Accept only Value Objects (VOs) or Domain Entities as method parameters. Raw primitives MUST NOT be used in signatures.
- MUST: Return objects or boolean wrapped over .
Result<T, Errors> - MUST: Use explicit Error Unions for the return type.
- MUST: Support optional injected transactions for mutations ().
tx?: Transaction
typescript
// MUST: Accept VOs, explicit Error typing
export abstract class WorkspaceRepository {
// MUST: Require scope context (userId)
abstract list(params: {
userId: UUID;
itemsPerPage: PositiveInteger;
page: PositiveInteger;
}): Promise<Result<Workspace[], RepositoryError>>;
// MUST: Require scope context (userId)
// Method Overloading: Repositories MAY expose overloaded signatures for different
// authorization levels or unique constraint checks.
abstract exists(params: { workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;
abstract exists(params: { userId: UUID; workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;
// MUST: Require scope context (userId)
abstract create(params: {
tx?: Transaction; // Strongly typed
userId: UUID;
workspace: Workspace;
}): Promise<Result<void, RepositoryError | UserNotFoundError>;
}参考示例:
references/examples/repository-interface-example.md- 必须:定义为,而非
abstract class。这是因为类符号可作为服务容器的注入令牌。interface - 必须:方法参数仅接受值对象(VOs)或领域实体。签名中不得使用原始基本类型。
- 必须:返回包裹在中的对象或布尔值。
Result<T, Errors> - 必须:返回类型使用明确的错误联合类型。
- 必须:支持可选的注入式事务用于变更操作()。
tx?: Transaction
typescript
// MUST: Accept VOs, explicit Error typing
export abstract class WorkspaceRepository {
// MUST: Require scope context (userId)
abstract list(params: {
userId: UUID;
itemsPerPage: PositiveInteger;
page: PositiveInteger;
}): Promise<Result<Workspace[], RepositoryError>>;
// MUST: Require scope context (userId)
// Method Overloading: Repositories MAY expose overloaded signatures for different
// authorization levels or unique constraint checks.
abstract exists(params: { workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;
abstract exists(params: { userId: UUID; workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;
// MUST: Require scope context (userId)
abstract create(params: {
tx?: Transaction; // Strongly typed
userId: UUID;
workspace: Workspace;
}): Promise<Result<void, RepositoryError | UserNotFoundError>>;
}3.1 Security via Scope Isolation
3.1 基于范围隔离的安全性
Security and multi-tenant data segmentation happen at the Repository level:
- MUST: Read/List/Exists/Update/Delete operations MUST require parent scope identifiers (e.g., ,
userId,workspaceId) alongside the target entity ID.folderId - By demanding parent scope IDs, the Repository physically isolates data via row-level queries (e.g.,
WHERE). If a user tries to alter an entity in another workspace, the DB responds cleanly as if the entity does not exist.AND folderId = ?
安全性和多租户数据隔离在仓库层实现:
- 必须:读/列表/存在性检查/更新/删除操作必须要求父范围标识符(如、
userId、workspaceId)与目标实体ID一同传入。folderId - 通过要求父范围ID,仓库可通过行级查询(如
WHERE)实现物理数据隔离。如果用户试图修改其他工作区中的实体,数据库会直接返回该实体不存在的结果。AND folderId = ?
3.2 Error Typology: Structural / Constraint Mappings
3.2 错误类型:结构/约束映射
Not every method fails in the exact same ways. Errors should be precisely typed:
- MUST (Constraint Errors): Repository methods that ADD or MODIFY information in the DB hitting structural constraints (like or
UNIQUEconstraints) MUST return domain-specific database errors (e.g.FOREIGN KEYor<Dependency>NotFoundError) parsed from the constraints mapped via Postgres error codes.<Field>AlreadyInUseError - MUST NOT (Read Errors): Standard Read or Exists operations WITHOUT constraint impacts MUST ONLY return . A purely failed read or a
RepositoryErrorlogic usually returns standard generic db connection failures. Do not bleed unnecessary Domain Errors on simple queries.count()
并非所有方法的失败方式都完全相同。错误应进行精确类型定义:
- 必须(约束错误):向数据库添加或修改信息时触发结构约束(如或
UNIQUE约束)的仓库方法,必须返回从Postgres错误码映射而来的特定领域数据库错误(如FOREIGN KEY或<Dependency>NotFoundError)。<Field>AlreadyInUseError - 禁止(读取错误):无约束影响的标准读取或存在性检查操作,只能返回。单纯的读取失败或
RepositoryError逻辑通常返回标准的通用数据库连接失败错误。不要在简单查询中暴露不必要的领域错误。count()
3.3 Method Overloading for Scope Flexibility
3.3 基于方法重载的范围灵活性
Repository methods MAY and SHOULD use TypeScript method overloading when different Use Cases require different levels of scope isolation or querying logic (e.g., an Admin role vs a User role, or querying by different unique constrained columns).
- MUST: Define clear overloads in the Interface.
- MUST: Implement them with union types or discriminate unions in the implementation class, ensuring the scope hierarchy is NEVER accidentally bypassed.
typescript
export abstract class WorkspaceRepository {
// Admin scope: no user restriction
abstract exists(params: { workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;
// User scope: isolated by userId
abstract exists(params: {
workspaceId: UUID;
userId: UUID;
}): Promise<Result<boolean, RepositoryError>>;
}当不同用例需要不同级别的范围隔离或查询逻辑时(如管理员角色与普通用户角色,或通过不同唯一约束列查询),仓库方法应当且可以使用TypeScript方法重载。
- 必须:在接口中定义清晰的重载。
- 必须:在实现类中使用联合类型或可区分联合类型实现重载,确保范围层级不会被意外绕过。
typescript
export abstract class WorkspaceRepository {
// Admin scope: no user restriction
abstract exists(params: { workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;
// User scope: isolated by userId
abstract exists(params: {
workspaceId: UUID;
userId: UUID;
}): Promise<Result<boolean, RepositoryError>>;
}4. Implementing the Postgres Repository
4. 实现Postgres仓库
Reference:
references/examples/repository-implementation-example.mdThe Implementation class transforms Drizzle database queries into values, taking raw Postgres strings and interpreting them cleanly as specific Domain Entities.
neverthrowerror.code- MUST: Unwrap (
ValueObjects) just before thevo.getValue(),.insert()or.update()clauses usingwhere.drizzle-orm
参考示例:
references/examples/repository-implementation-example.md实现类将Drizzle数据库查询转换为值,将原始Postgres字符串清晰解析为特定领域实体错误。
neverthrowerror.code- 必须:在使用执行
drizzle-orm、.insert()或.update()子句前,解包where(ValueObjects)。vo.getValue()
Handling Typed execution (executor
)
executor处理类型化执行器(executor
)
executor- Takes the primary pure database instance type from the ORM definition, strictly typed via , rather than loose
NodePgDatabase.typeof db - MUST: For any mutation method, explicitly declare the fallback executor: , then use
const executor = p.tx ?? this.db. Sinceexecutor.insert()fromTransactionmaps exactly to the PgTransaction DB pool, both TS types can be aligned easily without conflicts.@/shared/domain/types/withTransaction
- 从ORM定义中获取主纯数据库实例类型,通过进行严格类型定义,而非松散的
NodePgDatabase。typeof db - 必须:对于任何变更方法,显式声明回退执行器:,然后使用
const executor = p.tx ?? this.db。由于executor.insert()中的@/shared/domain/types/withTransaction与PgTransaction数据库池完全匹配,两种TS类型可轻松对齐无冲突。Transaction
Handling Postgres Errors
处理Postgres错误
- MUST NOT: Use for caught errors. The
anyblock defaults tocatch (error)in modern TypeScript.unknown - MUST: Validate instances. Drizzle and Postgres errors surface through mapping to a
DrizzleQueryErrorobject containing theDatabaseErrorandcodestrings.constraint - MUST: Compare against the typed constants in
error.cause.codefromPG_ERROR_CODES.@/shared/infrastructure/drizzle-postgres/pgErrorCodes
Instead of raw string parsing, node-postgres codes are matched:
- (Map to
PG_ERROR_CODES.UNIQUE_VIOLATION)....AlreadyInUseError - (Map to
PG_ERROR_CODES.FOREIGN_KEY_VIOLATION)....NotFoundError
Always verify naming patterns (defined natively in schema/drizzle indexing via exports) to figure out which foreign key or unique node failed.
MUST NOT: Use magic strings for . Instead, explicitly export and import the constraint names from the schema file (e.g., ).
error.cause.constrainterror.cause.constraintFK_WORKSPACES_USERtypescript
import { DrizzleQueryError } from "drizzle-orm";
import { DatabaseError } from "pg";
import { PG_ERROR_CODES } from "@/shared/infrastructure/drizzle-postgres/pgErrorCodes";
import { FK_WORKSPACES_USER } from "@/shared/infrastructure/drizzle-postgres/schema";
// ...
async create({ tx, userId, workspace }: CreateParams): Promise<Result<void, CreateErrors>> {
const executor = tx ?? this.db;
try {
await executor.insert(workspaces).values({
id: workspace.getId().getValue(),
name: workspace.getName().getValue(),
});
return ok(undefined);
} catch (error) {
if (error instanceof DrizzleQueryError && error.cause instanceof DatabaseError) {
if (
error.cause.code === PG_ERROR_CODES.FOREIGN_KEY_VIOLATION &&
error.cause.constraint === FK_WORKSPACES_USER
) {
return err(new UserNotFoundError({ id: userId, cause: error }));
}
}
return err(new RepositoryError(error as Error));
}
}- 禁止:捕获错误时使用类型。现代TypeScript中
any块默认类型为catch (error)。unknown - 必须:验证实例。Drizzle和Postgres错误通过映射到包含
DrizzleQueryError和code字符串的constraint对象。DatabaseError - 必须:将与
error.cause.code中的类型化常量@/shared/infrastructure/drizzle-postgres/pgErrorCodes进行比较。PG_ERROR_CODES
避免原始字符串解析,直接匹配node-postgres错误码:
- (映射为
PG_ERROR_CODES.UNIQUE_VIOLATION)。...AlreadyInUseError - (映射为
PG_ERROR_CODES.FOREIGN_KEY_VIOLATION)。...NotFoundError
务必验证的命名模式(通过schema/drizzle索引导出原生定义),以确定哪个外键或唯一约束节点失败。
禁止:为使用魔法字符串。应从schema文件中显式导出并导入约束名称(如)。
error.cause.constrainterror.cause.constraintFK_WORKSPACES_USERtypescript
import { DrizzleQueryError } from "drizzle-orm";
import { DatabaseError } from "pg";
import { PG_ERROR_CODES } from "@/shared/infrastructure/drizzle-postgres/pgErrorCodes";
import { FK_WORKSPACES_USER } from "@/shared/infrastructure/drizzle-postgres/schema";
// ...
async create({ tx, userId, workspace }: CreateParams): Promise<Result<void, CreateErrors>> {
const executor = tx ?? this.db;
try {
await executor.insert(workspaces).values({
id: workspace.getId().getValue(),
name: workspace.getName().getValue(),
});
return ok(undefined);
} catch (error) {
if (error instanceof DrizzleQueryError && error.cause instanceof DatabaseError) {
if (
error.cause.code === PG_ERROR_CODES.FOREIGN_KEY_VIOLATION &&
error.cause.constraint === FK_WORKSPACES_USER
) {
return err(new UserNotFoundError({ id: userId, cause: error }));
}
}
return err(new RepositoryError(error as Error));
}
}Zod validations and Testing Strategy Notes (Docker/PG)
Zod验证与测试策略说明(Docker/PG)
- Drizzle schema definition arrays in this stack rely on configurations managed via and mapped via UI
drizzle-kitlater.zod - Postgres specific integration tests tied to this class run connected via running standard
testcontainersPostgres tests with isolated rollback strategies (seedockerskill).testing
- 本技术栈中的Drizzle schema定义数组依赖于管理的配置,后续通过UI
drizzle-kit进行映射。zod - 与该类关联的Postgres特定集成测试通过运行标准
testcontainersPostgres测试,并采用隔离回滚策略(详见docker技能)。testing