repositories

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Repositories

仓库

This skill documents how to structure Repository Interfaces in the Domain and Concrete Implementations in Infrastructure using our tightly coupled stack:
drizzle-orm
,
node-postgres
,
drizzle-kit
, and Railway-Oriented Programming (
neverthrow
).
DEPENDENCY NOTICE: This skill works in tandem with the
use-cases
skill. Review both when managing vertical slices of domain logic.
本技能文档介绍如何使用我们紧密耦合的技术栈:
drizzle-orm
node-postgres
drizzle-kit
和面向铁路编程(
neverthrow
),在领域层构建仓库接口,在基础设施层实现具体实例
依赖说明:本技能需与
use-cases
技能配合使用。在管理领域逻辑垂直切片时,请同时参考这两项技能。

1. 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
    entityRepository.ts
    containing the inner
    EntityRepository
    class.
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
    abstract class
    , NOT an
    interface
    . This is required so the class symbol can be used as an injection token by our service containers.
  • 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
    ,
    folderId
    ) alongside the target entity ID.
  • By demanding parent scope IDs, the Repository physically isolates data via row-level
    WHERE
    queries (e.g.,
    AND folderId = ?
    ). If a user tries to alter an entity in another workspace, the DB responds cleanly as if the entity does not exist.
安全性和多租户数据隔离在仓库层实现:
  • 必须:读/列表/存在性检查/更新/删除操作必须要求父范围标识符(如
    userId
    workspaceId
    folderId
    )与目标实体ID一同传入。
  • 通过要求父范围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
    UNIQUE
    or
    FOREIGN KEY
    constraints) MUST return domain-specific database errors (e.g.
    <Dependency>NotFoundError
    or
    <Field>AlreadyInUseError
    ) parsed from the constraints mapped via Postgres error codes.
  • MUST NOT (Read Errors): Standard Read or Exists operations WITHOUT constraint impacts MUST ONLY return
    RepositoryError
    . A purely failed read or a
    count()
    logic usually returns standard generic db connection failures. Do not bleed unnecessary Domain Errors on simple queries.
并非所有方法的失败方式都完全相同。错误应进行精确类型定义:
  • 必须(约束错误):向数据库添加或修改信息时触发结构约束(如
    UNIQUE
    FOREIGN KEY
    约束)的仓库方法,必须返回从Postgres错误码映射而来的特定领域数据库错误(如
    <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.md
The Implementation class transforms Drizzle database queries into
neverthrow
values, taking raw Postgres
error.code
strings and interpreting them cleanly as specific Domain Entities.
  • MUST: Unwrap
    ValueObjects
    (
    vo.getValue()
    ) just before the
    .insert()
    ,
    .update()
    or
    where
    clauses using
    drizzle-orm
    .
参考示例
references/examples/repository-implementation-example.md
实现类将Drizzle数据库查询转换为
neverthrow
值,将原始Postgres
error.code
字符串清晰解析为特定领域实体错误。
  • 必须:在使用
    drizzle-orm
    执行
    .insert()
    .update()
    where
    子句前,解包
    ValueObjects
    vo.getValue()
    )。

Handling Typed execution (
executor
)

处理类型化执行器(
executor

  • Takes the primary pure database instance type from the ORM definition, strictly typed via
    NodePgDatabase
    , rather than loose
    typeof db
    .
  • MUST: For any mutation method, explicitly declare the fallback executor:
    const executor = p.tx ?? this.db
    , then use
    executor.insert()
    . Since
    Transaction
    from
    @/shared/domain/types/withTransaction
    maps exactly to the PgTransaction DB pool, both TS types can be aligned easily without conflicts.
  • 从ORM定义中获取主纯数据库实例类型,通过
    NodePgDatabase
    进行严格类型定义,而非松散的
    typeof db
  • 必须:对于任何变更方法,显式声明回退执行器:
    const executor = p.tx ?? this.db
    ,然后使用
    executor.insert()
    。由于
    @/shared/domain/types/withTransaction
    中的
    Transaction
    与PgTransaction数据库池完全匹配,两种TS类型可轻松对齐无冲突。

Handling Postgres Errors

处理Postgres错误

  • MUST NOT: Use
    any
    for caught errors. The
    catch (error)
    block defaults to
    unknown
    in modern TypeScript.
  • MUST: Validate instances. Drizzle and Postgres errors surface through
    DrizzleQueryError
    mapping to a
    DatabaseError
    object containing the
    code
    and
    constraint
    strings.
  • MUST: Compare
    error.cause.code
    against the typed constants in
    PG_ERROR_CODES
    from
    @/shared/infrastructure/drizzle-postgres/pgErrorCodes
    .
Instead of raw string parsing, node-postgres codes are matched:
  • PG_ERROR_CODES.UNIQUE_VIOLATION
    (Map to
    ...AlreadyInUseError
    ).
  • PG_ERROR_CODES.FOREIGN_KEY_VIOLATION
    (Map to
    ...NotFoundError
    ).
Always verify
error.cause.constraint
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
error.cause.constraint
. Instead, explicitly export and import the constraint names from the schema file (e.g.,
FK_WORKSPACES_USER
).
typescript
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));
    }
  }
  • 禁止:捕获错误时使用
    any
    类型。现代TypeScript中
    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
    )。
务必验证
error.cause.constraint
的命名模式(通过schema/drizzle索引导出原生定义),以确定哪个外键或唯一约束节点失败。 禁止:为
error.cause.constraint
使用魔法字符串。应从schema文件中显式导出并导入约束名称(如
FK_WORKSPACES_USER
)。
typescript
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
    drizzle-kit
    and mapped via UI
    zod
    later.
  • Postgres specific integration tests tied to this class run connected via
    testcontainers
    running standard
    docker
    Postgres tests with isolated rollback strategies (see
    testing
    skill).
  • 本技术栈中的Drizzle schema定义数组依赖于
    drizzle-kit
    管理的配置,后续通过UI
    zod
    进行映射。
  • 与该类关联的Postgres特定集成测试通过
    testcontainers
    运行标准
    docker
    Postgres测试,并采用隔离回滚策略(详见
    testing
    技能)。