use-cases

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Use Cases

用例(Use Cases)

This skill documents how to structure Use Cases using Domain-Driven Design (DDD) and Railway-Oriented Programming via
neverthrow
. Use Cases orchestrate business logic against repositories while maintaining strict domain contracts: they work exclusively with Value Objects, never primitives.
DEPENDENCY NOTICE: This skill works in tandem with the
repositories
skill. For repository interface/implementation details, read the
repositories
skill.
本技能文档介绍如何通过
neverthrow
,使用领域驱动设计(DDD)面向铁路编程(Railway-Oriented Programming)来构建Use Cases(用例)。Use Cases协调与仓库(repositories)相关的业务逻辑,同时维护严格的领域契约:它们仅与**Value Objects(值对象)**交互,绝不使用原始类型。
依赖说明:本技能需与
repositories
技能配合使用。关于仓库的接口/实现细节,请阅读
repositories
技能文档。

1. Naming Convention

1. 命名规范

Use Case class names MUST follow the pattern:
<Action><Entity>[For<Subject>]
  • Simple case:
    ListWorkspaces
    ,
    CreateUser
    ,
    ExistWorkspace
  • Multi-actor case: If different security/authorization requirements exist for the same action and entity, create separate Use Cases:
    • CreateWorkspaceForUser
      — user creates personal workspace
    • CreateWorkspaceForAdmin
      — admin creates workspace for tenant
  • Rationale: Each Use Case has distinct authorization, validation, and error handling requirements. Separate classes ensure no accidental coupling of disparate business logic.
Use Case类名必须遵循以下模式:
<Action><Entity>[For<Subject>]
  • 简单场景
    ListWorkspaces
    CreateUser
    ExistWorkspace
  • 多角色场景:如果同一操作和实体存在不同的安全/授权要求,请创建独立的Use Cases:
    • CreateWorkspaceForUser
      —— 用户创建个人工作区
    • CreateWorkspaceForAdmin
      —— 管理员为租户创建工作区
  • 设计理由:每个Use Case都有独特的授权、验证和错误处理要求。独立类可避免不同业务逻辑意外耦合。

2. Core Principles

2. 核心原则

Value Objects Only — Never Primitives

仅使用Value Objects——禁止原始类型

  • Params type: Use Case params MUST accept only Value Objects (VOs) and domain types. Primitive types (
    string
    ,
    number
    ,
    boolean
    ) MUST NOT be used.
    • ✅ Example:
      workspaceName: WorkspaceName
    • ❌ Wrong:
      name: string
  • Return type & Entities:
    execute()
    MUST return domain Entities or arrays of Entities wrapped in
    Result<T, ErrorUnion>
    . Primitive types MUST NOT be returned.
    • ✅ Example:
      Promise<Result<Workspace, CreateWorkspaceErrors>>
    • ❌ Wrong:
      Promise<Result<string, Error>>
  • Snapshots for private data exclusion: If an Entity contains truly private/internal data that MUST NOT be exposed (e.g., password hash, internal security state), a Snapshot type MAY be defined and returned instead. The Snapshot is a flat object containing exclusively Value Objects (extracted from the Entity) without the private fields.
    • Snapshot definition: MUST be defined in the same file as the Use Case.
    • Use Case responsibility: Exclude truly private data (cryptographic hashes, internal state). The presentation/infrastructure layer CAN serialize, filter, and transform fields as needed.
    • ✅ Example:
      Promise<Result<UserSnapshot, CreateUserErrors>>
      where
      UserSnapshot
      has
      userId: UUID
      ,
      email: Email
      but NOT the password hash.
    • ❌ Wrong: Returning the full
      User
      Entity with password hash exposed.
typescript
// Define snapshot in the same Use Case file
export type UserSnapshot = {
  userId: UUID;
  email: Email;
  name: UserName;
  // ❌ NOT included: passwordHash (private data)
};

export class CreateUser {
  // ... execute() returns Promise<Result<UserSnapshot, CreateUserErrors>>
}
  • 参数类型:Use Case参数必须仅接受**Value Objects(值对象)**和领域类型。禁止使用原始类型(
    string
    number
    boolean
    )。
    • ✅ 示例:
      workspaceName: WorkspaceName
    • ❌ 错误示例:
      name: string
  • 返回类型与实体
    execute()
    必须返回包装在
    Result<T, ErrorUnion>
    中的领域实体或实体数组。禁止返回原始类型。
    • ✅ 示例:
      Promise<Result<Workspace, CreateWorkspaceErrors>>
    • ❌ 错误示例:
      Promise<Result<string, Error>>
  • 快照用于排除私有数据:如果实体包含绝对不能暴露的私有/内部数据(如密码哈希、内部安全状态),则可以定义并返回**Snapshot(快照)**类型。快照是仅包含从实体中提取的Value Objects的扁平对象,不含私有字段。
    • 快照定义:必须在Use Case的同一文件中定义。
    • Use Case职责:仅排除真正的私有数据(加密哈希、内部状态)。展示/基础设施层可根据需要进行序列化、过滤和字段转换。
    • ✅ 示例:
      Promise<Result<UserSnapshot, CreateUserErrors>>
      ,其中
      UserSnapshot
      包含
      userId: UUID
      email: Email
      ,但不包含密码哈希。
    • ❌ 错误示例:返回包含密码哈希的完整
      User
      实体。
typescript
// Define snapshot in the same Use Case file
export type UserSnapshot = {
  userId: UUID;
  email: Email;
  name: UserName;
  // ❌ NOT included: passwordHash (private data)
};

export class CreateUser {
  // ... execute() returns Promise<Result<UserSnapshot, CreateUserErrors>>
}

Session Management via Parameters

通过参数管理会话

  • getSession
    MUST be received as a parameter in the
    execute()
    method signature IF authentication is required for this Use Case operation.
    getSession
    MUST NOT be injected into the constructor.
    • When optional: Public Use Cases (e.g.,
      LoginUser
      ,
      RegisterUser
      ) that allow unauthenticated access MUST NOT include
      getSession
      in params.
    • When required: Protected Use Cases (e.g.,
      CreateWorkspace
      ,
      ListWorkspaces
      ) that require user context MUST include
      getSession
      in params.
    • Rationale: Passing
      getSession
      at invocation time ensures each call evaluates auth dynamically, preventing stale session state.
    • Pattern:
      async execute({ ...params, getSession }: Params): Promise<Result<T, Errors>>
  • When
    getSession
    is included, session verification MUST be the first operation in
    execute()
    . All subsequent logic depends on session validity.
  • 如果Use Case操作需要认证,则
    execute()
    方法签名必须以参数形式接收
    getSession
    getSession
    绝不能注入到构造函数中。
    • 可选场景:允许未认证访问的公共Use Cases(如
      LoginUser
      RegisterUser
      )不得在参数中包含
      getSession
    • 必填场景:需要用户上下文的受保护Use Cases(如
      CreateWorkspace
      ListWorkspaces
      )必须在参数中包含
      getSession
    • 设计理由:在调用时传递
      getSession
      可确保每次调用动态评估认证状态,避免会话状态过期。
    • 模式
      async execute({ ...params, getSession }: Params): Promise<Result<T, Errors>>
  • 如果包含
    getSession
    ,会话验证必须是
    execute()
    中的第一个操作。后续所有逻辑都依赖于会话的有效性。

Railway-Oriented Error Handling

面向铁路的错误处理

  • execute()
    MUST return
    Promise<Result<T, ErrorUnion>>
    via
    neverthrow
    .
  • Exceptions MUST NOT be thrown manually for domain or business logic errors. All failures MUST be wrapped in
    err(error)
    .
  • Result values MUST be destructured using
    .isErr()
    before proceeding. Every async operation returning
    Result
    MUST be checked for errors.
  • execute()
    必须通过
    neverthrow
    返回
    Promise<Result<T, ErrorUnion>>
  • 绝不能为领域或业务逻辑错误手动抛出异常。所有失败必须包装在
    err(error)
    中。
  • 在继续处理前,必须使用
    .isErr()
    解构Result值。每个返回
    Result
    的异步操作都必须检查错误。

Dependency Injection via Constructor

通过构造函数注入依赖

  • Repositories and
    WithTransaction
    MUST be injected in the constructor, not instantiated within
    execute()
    .
  • Dependencies MUST be stored as
    private readonly
    fields and remain immutable.
  • 仓库和
    WithTransaction
    必须注入到构造函数中,而不是在
    execute()
    中实例化。
  • 依赖必须存储为
    private readonly
    字段,且保持不可变。

No Dependency Between Use Cases

Use Cases之间无依赖

  • Use Cases MUST NEVER depend on other Use Cases. They orchestrate repositories, not each other.
    • ✅ Example:
      execute()
      calls
      this.userRepo.create()
      and
      this.workspaceRepo.create()
      .
    • ❌ Wrong:
      CreateWorkspace
      injecting and calling
      CreateFolder.execute()
      .
  • Rationale: Chaining use cases muddles transactional boundaries, creates circular dependencies, and couples error handling contexts together unnecessarily.
  • Use Cases绝不能依赖其他Use Cases。它们协调仓库,而非彼此。
    • ✅ 示例:
      execute()
      调用
      this.userRepo.create()
      this.workspaceRepo.create()
    • ❌ 错误示例:
      CreateWorkspace
      注入并调用
      CreateFolder.execute()
  • 设计理由:链式调用Use Cases会混淆事务边界,创建循环依赖,并不必要地耦合错误处理上下文。

Single Public Method

仅暴露单个公共方法

  • Each Use Case class MUST expose exactly one public method:
    async execute(Params): Promise<Result<T, Errors>>
    .
  • Complex logic MAY be split into
    private
    methods within the same class for readability and maintainability.
  • 每个Use Case类必须仅暴露一个公共方法:
    async execute(Params): Promise<Result<T, Errors>>
  • 为了可读性和可维护性,复杂逻辑可以拆分为同一类中的
    private
    方法。

3. Common Patterns

3. 常见模式

Most Use Cases follow one of four patterns. Working examples are in
references/examples/
:
大多数Use Cases遵循以下四种模式之一。可运行示例位于
references/examples/
目录下:

Pattern 1: CREATE — Multi-step Orchestration

模式1:CREATE——多步骤编排

File:
references/examples/create-example.md
When to use: Creating entities that require coordination across multiple repositories or validation tables.
Characteristics:
  • Receives multiple Value Objects as params.
  • Orchestrates multiple repository operations within a transaction boundary when REQUIRED for data consistency.
  • Returns a domain Entity.
  • Important Note on Errors: Repository methods that ADD new information to the database must handle structural constraints and return domain-specific database errors alongside the standard
    RepositoryError
    :
    • Unique Constraints: e.g., creating a URL with a path that must be unique maps to
      PathAlreadyInUseError
      .
    • Foreign Key Constraints: e.g., attaching to missing parents maps to
      <Dependency>NotFoundError
      . Standard creations without these constraints typically only return
      RepositoryError
      .
  • Transactions: Use
    withTransaction(async (tx) => { ... })
    ONLY when:
    • Multiple repositories are modified in the same operation (e.g., create workspace + initial folder)
    • Partial failures would leave data in an inconsistent state
    • Atomicity of multiple operations is a business requirement
    • NOT recommended for single-table creations (see: unnecessary overhead)
Key steps:
  1. Session verification (if
    getSession
    param included) — MUST be first.
  2. Create domain entities using constructors.
  3. If multi-step: wrap in transaction, check
    .isErr()
    on each result.
  4. Return created entity or error.
文件
references/examples/create-example.md
适用场景:创建需要跨多个仓库或验证表协调的实体。
特征
  • 接收多个Value Objects作为参数。
  • 当数据一致性要求时,在事务边界内协调多个仓库操作。
  • 返回领域实体
  • 错误注意事项:向数据库添加新信息的仓库方法必须处理结构约束,并返回特定于领域的数据库错误以及标准
    RepositoryError
    • 唯一约束:例如,创建路径必须唯一的URL时,对应
      PathAlreadyInUseError
    • 外键约束:例如,关联不存在的父资源时,对应
      <Dependency>NotFoundError
      。 无这些约束的标准创建操作通常仅返回
      RepositoryError
  • 事务:仅在以下场景使用
    withTransaction(async (tx) => { ... })
    • 同一操作中修改多个仓库(如创建工作区+初始文件夹)
    • 部分失败会导致数据处于不一致状态
    • 多个操作的原子性是业务需求
    • 不推荐用于单表创建(避免不必要的开销)
关键步骤
  1. 会话验证(如果包含
    getSession
    参数)——必须是第一步。
  2. 使用构造函数创建领域实体。
  3. 如果是多步骤:包装在事务中,检查每个结果的
    .isErr()
  4. 返回创建的实体或错误。

Pattern 2: LIST — Query with Pagination & Validation

模式2:LIST——带分页与验证的查询

File:
references/examples/list-example.md
When to use: Retrieving paginated collections with business rule validation.
Characteristics:
  • List operations MUST include pagination to prevent unbounded query results.
  • Pagination MAY be offset-based (
    page
    ,
    itemsPerPage
    ) or cursor-based (
    cursor
    ,
    limit
    ).
  • Receives pagination params as Value Objects (
    PositiveInteger
    ,
    Cursor
    , etc.).
  • Validates business constraints (pagination limits) BEFORE repository call.
  • When listing within a hierarchical scope, MUST verify parent scope access first (e.g., verify user owns workspace before listing folders).
  • Returns array of Entities:
    Entity[]
    .
  • Transactions: MUST NOT be used. Reads are inherently consistent.
Key steps:
  1. Validate pagination params (e.g.,
    itemsPerPage <= maxItemsPerPage
    or cursor validity).
  2. Session verification (if
    getSession
    param included).
  3. Optional: Verify scope hierarchy via
    ensureIsDataConsistent()
    (if resource is in a hierarchy).
  4. Delegate to repository's
    list()
    method.
文件
references/examples/list-example.md
适用场景:检索带有业务规则验证的分页集合。
特征
  • 列表操作必须包含分页,以避免无限制的查询结果。
  • 分页可以是基于偏移量的(
    page
    itemsPerPage
    )或基于游标(
    cursor
    limit
    )。
  • 接收作为Value Objects的分页参数(
    PositiveInteger
    Cursor
    等)。
  • 在调用仓库前验证业务约束(分页限制)。
  • 在分层范围内列出资源时,必须先验证父范围的访问权限(如在列出文件夹前验证用户拥有工作区)。
  • 返回实体数组:
    Entity[]
  • 事务:禁止使用。读取操作本身具有一致性。
关键步骤
  1. 验证分页参数(如
    itemsPerPage <= maxItemsPerPage
    或游标有效性)。
  2. 会话验证(如果包含
    getSession
    参数)。
  3. 可选:通过
    ensureIsDataConsistent()
    验证范围层级(如果资源属于分层结构)。
  4. 委托给仓库的
    list()
    方法。

Pattern 3: EXISTS — Authorization Check

模式3:EXISTS——授权检查

File:
references/examples/exists-example.md
When to use: Checking if a resource exists and belongs to the current user.
Characteristics:
  • Minimal logic: auth + repository query.
  • Returns
    boolean
    wrapped in
    Result
    .
  • Transactions: MUST NOT be used. Single read operation.
Key steps:
  1. Session verification (if
    getSession
    param included).
  2. Delegate to repository's
    exists()
    method.
文件
references/examples/exists-example.md
适用场景:检查资源是否存在并属于当前用户。
特征
  • 逻辑极简:认证 + 仓库查询。
  • 返回包装在
    Result
    中的
    boolean
  • 事务:禁止使用。仅为单次读取操作。
关键步骤
  1. 会话验证(如果包含
    getSession
    参数)。
  2. 委托给仓库的
    exists()
    方法。

Pattern 4: COUNT — Aggregation with Scope Verification

模式4:COUNT——带范围验证的聚合

File:
references/examples/count-example.md
When to use: Retrieving aggregate counts for resources within a hierarchical scope.
Characteristics:
  • Returns a Value Object representing count (
    NonNegativeInteger
    , not primitive
    number
    ).
  • MUST verify scope hierarchy before delegation (e.g., user access to workspace, workspace access to folder).
  • Validates parent scopes exist before counting child resources.
  • Transactions: MUST NOT be used. Read-only aggregation operation.
Key steps:
  1. Session verification (if
    getSession
    param included).
  2. Verify scope hierarchy via
    ensureIsDataConsistent()
    (each repository verifies immediate parent scope).
  3. If hierarchy valid, delegate to repository's
    count()
    method.
Example hierarchy: User → Workspace → Folder → ShortUrl
  • Verify: Does user own workspace? → Does workspace own folder? → Count items in folder.
  • Each repository call passes only its immediate parent scope (
    { workspaceId, userId }
    , then
    { folderId, workspaceId }
    ).
文件
references/examples/count-example.md
适用场景:检索分层范围内资源的聚合计数。
特征
  • 返回表示计数的Value Object
    NonNegativeInteger
    ,而非原始
    number
    )。
  • 在委托前必须验证范围层级(如用户对工作区的访问权限、工作区对文件夹的访问权限)。
  • 在统计子资源前验证父范围存在。
  • 事务:禁止使用。仅为只读聚合操作。
关键步骤
  1. 会话验证(如果包含
    getSession
    参数)。
  2. 通过
    ensureIsDataConsistent()
    验证范围层级(每个仓库验证其直接父范围)。
  3. 如果层级有效,委托给仓库的
    count()
    方法。
示例层级:User → Workspace → Folder → ShortUrl
  • 验证流程:用户是否拥有工作区?→ 工作区是否拥有该文件夹?→ 统计文件夹中的项。
  • 每个仓库调用仅传递其直接父范围(
    { workspaceId, userId }
    ,然后是
    { folderId, workspaceId }
    )。

Pattern 5: UPDATE — Modifying Resources

模式5:UPDATE——修改资源

File:
references/examples/update-example.md
When to use: Modifying existing entity fields without changing ownership logic.
Characteristics:
  • MUST include all parent level IDs the same as CREATE/DELETE.
  • Accepts both required IDs and optional Value Objects representing fields to update.
  • Important Note on Errors: Repository methods that MODIFY structural constraints must return domain-specific database errors alongside standard
    RepositoryError
    :
    • Unique Constraints: e.g., updating a path to one that is already occupied maps to
      PathAlreadyInUseError
      .
    • Foreign Key Constraints: e.g., moving a short URL to a folder that was deleted maps to
      FolderNotFoundError
      . Standard updates without constraint changes usually just return
      RepositoryError
      .
  • Transactions: MUST NOT be used for single-resource updates.
Key steps:
  1. Session verification (if
    getSession
    param included).
  2. Verify scope hierarchy via
    ensureIsDataConsistent()
    (passing all parent IDs).
  3. Delegate to repository's
    update()
    method.
文件
references/examples/update-example.md
适用场景:修改现有实体字段,不改变所有权逻辑。
特征
  • 必须包含与CREATE/DELETE相同的所有父级ID。
  • 接收必填ID和表示待更新字段的可选Value Objects。
  • 错误注意事项:修改结构约束的仓库方法必须返回特定于领域的数据库错误以及标准
    RepositoryError
    • 唯一约束:例如,将路径更新为已占用的路径时,对应
      PathAlreadyInUseError
    • 外键约束:例如,将短URL移动到已删除的文件夹时,对应
      FolderNotFoundError
      。 无约束变更的标准更新操作通常仅返回
      RepositoryError
  • 事务:禁止用于单资源更新。
关键步骤
  1. 会话验证(如果包含
    getSession
    参数)。
  2. 通过
    ensureIsDataConsistent()
    验证范围层级(传递所有父级ID)。
  3. 委托给仓库的
    update()
    方法。

Pattern 6: DELETE — Hard vs Soft Deletion

模式6:DELETE——硬删除与软删除

File:
references/examples/delete-example.md
When to use: Removing resources or marking them as unavailable.
Characteristics:
  • Hard Delete (Physical removal): Use for low-value data or leaf nodes (e.g.,
    ShortUrl
    ).
    • If deleting an intermediate node (e.g.,
      Folder
      ), you MUST consider foreign key constraints. You must handle child dependencies (either via DB-level
      ON DELETE CASCADE
      or via a transaction orchestrating multiple repository deletions).
    • Returns
      Result<void, Errors>
      .
    • Transactions: MUST NOT be used for single-table deletes. MUST be used if manually orchestrating child deletions.
  • Soft Delete (Logical removal): Use for high-value data, legal compliance, or recoverable resources.
    • MUST NOT use the
      Delete
      prefix to avoid ambiguity. Use explicit domain verbs like
      Archive<Entity>
      ,
      Deactivate<Entity>
      , or
      Trash<Entity>
      .
    • This is technically an
      UPDATE
      operation in the repository.
Key steps (Hard Delete):
  1. Session verification (if
    getSession
    param included).
  2. Verify scope hierarchy via
    ensureIsDataConsistent()
    (passing all parent IDs).
  3. Delegate to repository's
    delete()
    method.
文件
references/examples/delete-example.md
适用场景:移除资源或标记为不可用。
特征
  • 硬删除(物理移除):适用于低价值数据或叶子节点(如
    ShortUrl
    )。
    • 如果删除中间节点(如
      Folder
      ),必须考虑外键约束。必须处理子依赖(通过数据库级
      ON DELETE CASCADE
      或协调多个仓库删除的事务)。
    • 返回
      Result<void, Errors>
    • 事务:禁止用于单表删除。如果手动协调子删除,则必须使用事务。
  • 软删除(逻辑移除):适用于高价值数据、合规要求或可恢复资源。
    • 禁止使用
      Delete
      前缀以避免歧义。使用明确的领域动词,如
      Archive<Entity>
      Deactivate<Entity>
      Trash<Entity>
    • 这在仓库中本质上是
      UPDATE
      操作。
硬删除关键步骤
  1. 会话验证(如果包含
    getSession
    参数)。
  2. 通过
    ensureIsDataConsistent()
    验证范围层级(传递所有父级ID)。
  3. 委托给仓库的
    delete()
    方法。

4. Transaction Requirements

4. 事务要求

Transactions MUST be used only when necessary. Misuse causes performance degradation and unnecessary complexity.
Use transactions WHEN:
  • ✅ Multiple repositories are written in one Use Case
  • ✅ Data consistency across tables is a business requirement
  • ✅ Partial failure would violate domain invariants
MUST NOT use transactions WHEN:
  • ❌ Single read operation (no writes)
  • ❌ Single write to one table (no cross-table dependencies)
  • ❌ No business rule requires atomic writes
Example scenarios:
  • ListWorkspaces
    : No transaction (single read)
  • CreateUser
    (username + profile in one table): No transaction (single write)
  • CreateWorkspace
    (workspace + default folder): Yes transaction (two tables, atomicity required)
仅在必要时使用事务。滥用会导致性能下降和不必要的复杂度。
必须使用事务的场景
  • ✅ 单个Use Case中写入多个仓库
  • ✅ 跨表数据一致性是业务需求
  • ✅ 部分失败会违反领域不变量
禁止使用事务的场景
  • ❌ 单次读取操作(无写入)
  • ❌ 单表单次写入(无跨表依赖)
  • ❌ 无业务规则要求原子写入
示例场景
  • ListWorkspaces
    :无需事务(单次读取)
  • CreateUser
    (用户名+配置文件在同一表):无需事务(单次写入)
  • CreateWorkspace
    (工作区+默认文件夹):必须使用事务(两个表,需要原子性)

5. Type Definitions Pattern

5. 类型定义模式

Each Use Case MUST define two types (Params and Errors). Optionally, if the Entity contains private data that MUST NOT be exposed, define a Snapshot type:
typescript
// Example 1: Protected Use Case (requires authentication)
export type ListWorkspacesParams = {
  itemsPerPage: PositiveInteger;  // ✅ Value Object
  page: PositiveInteger;          // ✅ Value Object
  getSession: GetSession;         // ✅ Included if auth is required
};

// Example 2: Public Use Case (no authentication required)
export type LoginUserParams = {
  email: Email;                   // ✅ Value Object
  password: Password;             // ✅ Value Object
  // ❌ NO getSession: unauthenticated operation
};

// Errors: MUST be explicit and exhaustive Union
export type YourUseCaseErrors =
  | RepositoryError
  | SessionError
  | YourDomainSpecificError;

// Snapshot (optional): Defined in the same file, excludes private data
export type YourEntitySnapshot = {
  id: UUID;                       // ✅ Value Object
  name: SomeName;                 // ✅ Value Object
  // ❌ NOT included: internalPasswordHash, privateState
};
Return type choices:
  • Without private data:
    Promise<Result<Entity, YourUseCaseErrors>>
  • With private data to exclude:
    Promise<Result<EntitySnapshot, YourUseCaseErrors>>
每个Use Case必须定义两种类型(ParamsErrors)。如果实体包含必须隐藏的私有数据,可选择定义**Snapshot(快照)**类型:
typescript
// Example 1: Protected Use Case (requires authentication)
export type ListWorkspacesParams = {
  itemsPerPage: PositiveInteger;  // ✅ Value Object
  page: PositiveInteger;          // ✅ Value Object
  getSession: GetSession;         // ✅ Included if auth is required
};

// Example 2: Public Use Case (no authentication required)
export type LoginUserParams = {
  email: Email;                   // ✅ Value Object
  password: Password;             // ✅ Value Object
  // ❌ NO getSession: unauthenticated operation
};

// Errors: MUST be explicit and exhaustive Union
export type YourUseCaseErrors =
  | RepositoryError
  | SessionError
  | YourDomainSpecificError;

// Snapshot (optional): Defined in the same file, excludes private data
export type YourEntitySnapshot = {
  id: UUID;                       // ✅ Value Object
  name: SomeName;                 // ✅ Value Object
  // ❌ NOT included: internalPasswordHash, privateState
};
返回类型选择
  • 无私有数据:
    Promise<Result<Entity, YourUseCaseErrors>>
  • 需排除私有数据:
    Promise<Result<EntitySnapshot, YourUseCaseErrors>>

6. Authorization (Use Case Permission Verification + Repository Scope Isolation)

6. 授权(Use Case权限验证 + 仓库范围隔离)

Critical principle: Authorization has two distinct layers:
  1. Scope isolation (Repository): WHERE clauses prevent cross-user/cross-context data access.
  2. Permission verification (Use Case): Business logic validates that the authenticated user has permission to perform the action.
核心原则:授权分为两个独立层
  1. 范围隔离(仓库):WHERE子句防止跨用户/跨上下文数据访问。
  2. 权限验证(Use Case):业务逻辑验证认证用户是否有权执行操作。

Scope Isolation vs. Permission Verification

范围隔离 vs 权限验证

Scope Isolation (Repository Responsibility):
  • Applies WHERE clauses to enforce context boundaries.
  • Example:
    WHERE userId = #{currentUserId}
    prevents User A from accessing User B's data.
  • Handled via repository method parameters:
    exists({ workspaceId, userId })
    vs.
    exists({ workspaceId })
    for different access levels.
Permission Verification (Use Case Responsibility):
  • Validates the user has the specific role or permission to perform the action.
  • Example: User exists in workspace X, but lacks "Delete" permission → Use Case MUST reject.
  • MUST run after scope hierarchy verification.
范围隔离(仓库职责)
  • 应用WHERE子句以强制上下文边界。
  • 示例:
    WHERE userId = #{currentUserId}
    防止用户A访问用户B的数据。
  • 通过仓库方法参数实现:
    exists({ workspaceId, userId })
    vs
    exists({ workspaceId })
    对应不同访问级别。
权限验证(Use Case职责)
  • 验证用户是否具有执行操作的特定角色或权限
  • 示例:用户存在于工作区X,但缺少“删除”权限→Use Case必须拒绝。
  • 必须在范围层级验证之后执行。

Authorization Flow Example

授权流程示例

typescript
// Use Case: DeleteShortUrl
async execute({ shortUrlId, userId, workspaceId, folderId }: Params): Promise<Result<void, Errors>> {
  // 1. Verify user has "Delete" permission in this workspace
  const userRole = await this.permissionRepository.hasPermission({ workspaceId, userId, action: 'delete' });
  if (userRole.isErr()) {
    return err(userRole.error); // userRole.error is RepositoryError
  }
  if (!userRole.value) {
    return err(new UnauthorizedError({ userId, action: 'delete', resource: 'shortUrl' })); // user lacks delete permission
  }

  // 2. Verify hierarchy (Extracted to private method for readability)
  const consistencyResult = await this.ensureIsDataConsistent({ folderId, workspaceId });
  if (consistencyResult.isErr()) {
    return err(consistencyResult.error);
  }

  // 3. Perform the delete operation, which also enforces scope isolation via WHERE clauses
  return this.shortUrlRepository.delete({ folderId, shortUrlId });
}

private async ensureIsDataConsistent({ folderId, workspaceId }: { folderId: UUID; workspaceId: UUID }): Promise<Result<void, Errors>> {
  /* this.workspaceRepository.exists({ workspaceId, userId }); is not needed here because permission check already verifies user has access to workspace, but if we had a different Use Case that didn't require permission check, we would need to verify workspace access first before checking folder and short URL existence.*/

  const folderExistsResult = await this.folderRepository.exists({ folderId, workspaceId });
  if (folderExistsResult.isErr()) {
    return err(folderExistsResult.error); // folderExistsResult.error is RepositoryError
  }
  if (!folderExistsResult.value) {
    return err(new FolderNotFoundError({ folderId })); // folder doesn't exist in this workspace
  }

  return ok();
}
Key insight: Each repository call passes ONLY its immediate parent scope:
  • workspaceRepository.exists({ workspaceId, userId })
    → WHERE
    id = workspaceId AND userId = ?
  • folderRepository.exists({ folderId, workspaceId })
    → WHERE
    id = folderId AND workspaceId = ?
  • shortUrlRepository.exists({ shortUrlId, folderId })
    → WHERE
    id = shortUrlId AND folderId = ?
The Use Case orchestrates the hierarchy verification; each repository enforces its single-level scope isolation.
typescript
// Use Case: DeleteShortUrl
async execute({ shortUrlId, userId, workspaceId, folderId }: Params): Promise<Result<void, Errors>> {
  // 1. Verify user has "Delete" permission in this workspace
  const userRole = await this.permissionRepository.hasPermission({ workspaceId, userId, action: 'delete' });
  if (userRole.isErr()) {
    return err(userRole.error); // userRole.error is RepositoryError
  }
  if (!userRole.value) {
    return err(new UnauthorizedError({ userId, action: 'delete', resource: 'shortUrl' })); // user lacks delete permission
  }

  // 2. Verify hierarchy (Extracted to private method for readability)
  const consistencyResult = await this.ensureIsDataConsistent({ folderId, workspaceId });
  if (consistencyResult.isErr()) {
    return err(consistencyResult.error);
  }

  // 3. Perform the delete operation, which also enforces scope isolation via WHERE clauses
  return this.shortUrlRepository.delete({ folderId, shortUrlId });
}

private async ensureIsDataConsistent({ folderId, workspaceId }: { folderId: UUID; workspaceId: UUID }): Promise<Result<void, Errors>> {
  /* this.workspaceRepository.exists({ workspaceId, userId }); is not needed here because permission check already verifies user has access to workspace, but if we had a different Use Case that didn't require permission check, we would need to verify workspace access first before checking folder and short URL existence.*/

  const folderExistsResult = await this.folderRepository.exists({ folderId, workspaceId });
  if (folderExistsResult.isErr()) {
    return err(folderExistsResult.error); // folderExistsResult.error is RepositoryError
  }
  if (!folderExistsResult.value) {
    return err(new FolderNotFoundError({ folderId })); // folder doesn't exist in this workspace
  }

  return ok();
}
核心要点:每个仓库调用仅传递其直接父范围:
  • workspaceRepository.exists({ workspaceId, userId })
    → WHERE
    id = workspaceId AND userId = ?
  • folderRepository.exists({ folderId, workspaceId })
    → WHERE
    id = folderId AND workspaceId = ?
  • shortUrlRepository.exists({ shortUrlId, folderId })
    → WHERE
    id = shortUrlId AND folderId = ?
Use Case协调层级验证;每个仓库强制执行其单级范围隔离。

Scope Hierarchy Verification

范围层级验证

In hierarchical domains (e.g., User → Workspace → Folder → ShortUrl), the Use Case MUST verify each level. To achieve this, the Use Case
Params
MUST explicitly include the IDs for ALL parent levels in the hierarchy
(e.g.,
workspaceId
,
folderId
), regardless of how deep the target entity is. The Use Case MUST NOT rely on the database to infer or JOIN parent scopes; it must receive them as inputs to verify the unbroken chain.
User (from session)
  └─ Workspace (does user have access?)
      └─ Folder (does workspace own this folder?)
          └─ ShortUrl (does folder own this short URL?)
Example from
countShortUrls.ts
:
typescript
// Verification extracted to a private method for readability
private async ensureIsDataConsistent({
  userId,
  folderId,
  workspaceId,
}: {
  userId: UUID;
  workspaceId: UUID;
  folderId: UUID;
}): Promise<Result<void, Errors>> {
  // Verify the complete hierarchy
  const [workspaceExistsResult, folderExistsResult] = await Promise.all([
    this.workspaceRepository.exists({ workspaceId, userId }),        // ← Scope: user in workspace
    this.folderRepository.exists({ folderId, workspaceId }),         // ← Scope: folder in workspace
  ]);

  // If hierarchy is broken, the user cannot access the short URLs
  if (workspaceExistsResult.isErr()) {
    return err(workspaceExistsResult.error); // workspaceExistsResult.error is RepositoryError
  }
  if (!workspaceExistsResult.value) {
    return err(new WorkspaceNotFoundError({ workspaceId })); // user doesn't have access to this workspace
  }

  if (folderExistsResult.isErr()) {
    return err(folderExistsResult.error); // folderExistsResult.error is RepositoryError
  }
  if (!folderExistsResult.value) {
    return err(new FolderNotFoundError({ folderId })); // folder doesn't exist in this workspace
  }

  return ok();
}

// Then inside the core execute() method:
// const consistencyResult = await this.ensureIsDataConsistent({ userId, folderId, workspaceId });
// if (consistencyResult.isErr()) return err(consistencyResult.error);
// return this.shortUrlRepository.count({ folderId });
在分层领域中(如User → Workspace → Folder → ShortUrl),Use Case必须验证每个层级。为此,Use Case的
Params
必须明确包含层级中所有父级的ID
(如
workspaceId
folderId
),无论目标实体有多深。Use Case绝不能依赖数据库推断或JOIN父范围;必须将它们作为输入接收以验证完整链。
User (来自会话)
  └─ Workspace (用户是否有访问权限?)
      └─ Folder (工作区是否拥有该文件夹?)
          └─ ShortUrl (文件夹是否拥有该短URL?)
来自
countShortUrls.ts
的示例
typescript
// Verification extracted to a private method for readability
private async ensureIsDataConsistent({
  userId,
  folderId,
  workspaceId,
}: {
  userId: UUID;
  workspaceId: UUID;
  folderId: UUID;
}): Promise<Result<void, Errors>> {
  // Verify the complete hierarchy
  const [workspaceExistsResult, folderExistsResult] = await Promise.all([
    this.workspaceRepository.exists({ workspaceId, userId }),        // ← Scope: user in workspace
    this.folderRepository.exists({ folderId, workspaceId }),         // ← Scope: folder in workspace
  ]);

  // If hierarchy is broken, the user cannot access the short URLs
  if (workspaceExistsResult.isErr()) {
    return err(workspaceExistsResult.error); // workspaceExistsResult.error is RepositoryError
  }
  if (!workspaceExistsResult.value) {
    return err(new WorkspaceNotFoundError({ workspaceId })); // user doesn't have access to this workspace
  }

  if (folderExistsResult.isErr()) {
    return err(folderExistsResult.error); // folderExistsResult.error is RepositoryError
  }
  if (!folderExistsResult.value) {
    return err(new FolderNotFoundError({ folderId })); // folder doesn't exist in this workspace
  }

  return ok();
}

// Then inside the core execute() method:
// const consistencyResult = await this.ensureIsDataConsistent({ userId, folderId, workspaceId });
// if (consistencyResult.isErr()) return err(consistencyResult.error);
// return this.shortUrlRepository.count({ folderId });

Repository Support for Different Scopes

仓库对不同范围的支持

Repository methods MAY expose overloaded signatures for different authorization levels:
typescript
abstract class WorkspaceRepository {
  // Admin scope: no user restriction
  abstract exists(params: { workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;

  // User scope: must provide both user and workspace
  abstract exists(params: { 
    workspaceId: UUID; 
    userId: UUID; 
  }): Promise<Result<boolean, RepositoryError>>;
}
Use Case determines which scope to use:
  • DeleteWorkspace
    (admin): Calls
    workspaceRepository.exists({ workspaceId })
  • LeaveWorkspace
    (user): Calls
    workspaceRepository.exists({ workspaceId, userId })
仓库方法可以为不同授权级别提供重载签名:
typescript
abstract class WorkspaceRepository {
  // Admin scope: no user restriction
  abstract exists(params: { workspaceId: UUID }): Promise<Result<boolean, RepositoryError>>;

  // User scope: must provide both user and workspace
  abstract exists(params: { 
    workspaceId: UUID; 
    userId: UUID; 
  }): Promise<Result<boolean, RepositoryError>>;
}
Use Case决定使用哪个范围
  • DeleteWorkspace
    (管理员):调用
    workspaceRepository.exists({ workspaceId })
  • LeaveWorkspace
    (用户):调用
    workspaceRepository.exists({ workspaceId, userId })

MUST and MUST NOT Requirements

必须遵守与禁止的要求

  • MUST: Verify scope hierarchy in Use Case (to ensure data consistency across levels).
  • MUST: Verify user role/permissions in Use Case (business logic enforcement).
  • MUST: Apply scope-based WHERE clauses in Repository (to isolate users/contexts).
  • MUST NOT: Bypass scope verification in the Use Case.
  • MAY: Overload Repository methods only when different Use Cases genuinely require different scopes (admin vs. user patterns).
  • 必须:在Use Case中验证范围层级(确保跨层级的数据一致性)。
  • 必须:在Use Case中验证用户角色/权限(执行业务逻辑)。
  • 必须:在仓库中应用基于范围的WHERE子句(隔离用户/上下文)。
  • 禁止:在Use Case中跳过范围验证。
  • 可以:仅当不同Use Cases确实需要不同范围(管理员vs用户模式)时,才重载仓库方法。

7. Mandatory Scaffolding for New Projects

7. 新项目必备脚手架

If setting up these patterns in a new project using our standard stack (
drizzle-orm
,
zod
,
neverthrow
):
  1. Ensure
    WithTransaction
    with
    PgTransaction
    type is available in
    src/shared/domain/types/
    .
  2. Ensure
    withRetry
    function exists in
    src/shared/
    (for resilience patterns).
  3. Ensure
    GetSession
    type and related auth utilities are available in
    src/auth/
    .
If these do not exist, read
references/core-utilities.md
to scaffold the exact implementations.
如果在新项目中使用我们的标准技术栈(
drizzle-orm
zod
neverthrow
)设置这些模式:
  1. 确保
    src/shared/domain/types/
    目录下存在带
    PgTransaction
    类型的
    WithTransaction
  2. 确保
    src/shared/
    目录下存在
    withRetry
    函数(用于弹性模式)。
  3. 确保
    src/auth/
    目录下存在
    GetSession
    类型及相关认证工具。
如果这些不存在,请阅读
references/core-utilities.md
以搭建精确的实现。

8. RFC 2119 Language

8. 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.
  • MAY: Optional. Permitted if needed, but not required.
  • Example: "Use Case names MUST follow
    <Action><Entity>
    pattern." is non-negotiable.
本技能使用RFC 2119关键字来明确要求:
  • MUST / MUST NOT:强制性要求。违反会破坏正确性或安全性。
  • SHOULD / SHOULD NOT:强烈建议。只有有文档记录的理由时才可偏离。
  • MAY:可选。如有需要允许使用,但非强制。
  • 示例:“Use Case名称必须遵循
    <Action><Entity>
    模式”是不可协商的。

9. Anti-Patterns to Avoid

9. 需避免的反模式

❌ Anti-Pattern✅ Correct Pattern
getSession
in constructor
getSession
as execute parameter (if auth required)
Session check after business logicSession check as FIRST operation (if included)
Mixing primitives with VOs in paramsAll params are Value Objects
Throwing domain errors manuallyWrapping in
err(error)
via neverthrow
Transacting single-table writesOnly multi-step/multi-table writes need transactions
Multiple public methods per classSingle
execute()
method only
Direct dependency instantiationConstructor dependency injection
Returning Entities with private data exposedReturn Snapshot type (defined in same file) without private fields
Use Case decides all presentation detailsUse Case excludes private data only; presentation layer transforms/filters as needed
Getting session for unauthenticated Use CasesOnly include
getSession
if authentication is required for that operation
Skipping role/permission checks in Use CaseMUST verify user has permission to perform the action (e.g., role-based or capability-based checks)
Same Use Case for different access levelsSeparate Use Cases:
DeleteWorkspace
(admin) vs
LeaveWorkspace
(user) with different scopes
Repository without scope-based WHERE clausesMUST enforce scope isolation via WHERE:
WHERE userId = ?
or
WHERE workspaceId = ?
❌ 反模式✅ 正确模式
构造函数中注入
getSession
getSession
作为execute参数(如果需要认证)
业务逻辑之后检查会话会话检查为第一个操作(如果包含)
参数中混合原始类型与VOs所有参数均为Value Objects
手动抛出领域错误通过neverthrow包装在
err(error)
单表写入使用事务仅多步骤/多表写入需要事务
每个类有多个公共方法仅单个
execute()
方法
直接实例化依赖构造函数依赖注入
返回暴露私有数据的实体返回不含私有字段的Snapshot类型(在同一文件中定义)
Use Case决定所有展示细节Use Case仅排除私有数据;展示层按需转换/过滤
未认证Use Cases获取会话仅当操作需要认证时才包含
getSession
Use Case中跳过角色/权限检查必须验证用户有权执行操作(如基于角色或能力的检查)
同一Use Case对应不同访问级别独立Use Cases:
DeleteWorkspace
(管理员)vs
LeaveWorkspace
(用户),使用不同范围
仓库无基于范围的WHERE子句必须通过WHERE子句强制范围隔离:
WHERE userId = ?
WHERE workspaceId = ?