use-cases
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUse Cases
用例(Use Cases)
This skill documents how to structure Use Cases using Domain-Driven Design (DDD) and Railway-Oriented Programming via . Use Cases orchestrate business logic against repositories while maintaining strict domain contracts: they work exclusively with Value Objects, never primitives.
neverthrowDEPENDENCY NOTICE: This skill works in tandem with the skill. For repository interface/implementation details, read the skill.
repositoriesrepositories本技能文档介绍如何通过,使用领域驱动设计(DDD)和面向铁路编程(Railway-Oriented Programming)来构建Use Cases(用例)。Use Cases协调与仓库(repositories)相关的业务逻辑,同时维护严格的领域契约:它们仅与**Value Objects(值对象)**交互,绝不使用原始类型。
neverthrow依赖说明:本技能需与技能配合使用。关于仓库的接口/实现细节,请阅读技能文档。
repositoriesrepositories1. Naming Convention
1. 命名规范
Use Case class names MUST follow the pattern:
<Action><Entity>[For<Subject>]- Simple case: ,
ListWorkspaces,CreateUserExistWorkspace - Multi-actor case: If different security/authorization requirements exist for the same action and entity, create separate Use Cases:
- — user creates personal workspace
CreateWorkspaceForUser - — admin creates workspace for tenant
CreateWorkspaceForAdmin
- 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、CreateUserExistWorkspace - 多角色场景:如果同一操作和实体存在不同的安全/授权要求,请创建独立的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) MUST NOT be used.boolean- ✅ Example:
workspaceName: WorkspaceName - ❌ Wrong:
name: string
- ✅ Example:
-
Return type & Entities:MUST return domain Entities or arrays of Entities wrapped in
execute(). Primitive types MUST NOT be returned.Result<T, ErrorUnion>- ✅ Example:
Promise<Result<Workspace, CreateWorkspaceErrors>> - ❌ Wrong:
Promise<Result<string, Error>>
- ✅ Example:
-
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: where
Promise<Result<UserSnapshot, CreateUserErrors>>hasUserSnapshot,userId: UUIDbut NOT the password hash.email: Email - ❌ Wrong: Returning the full Entity with password hash exposed.
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>>
}-
参数类型: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
通过参数管理会话
-
MUST be received as a parameter in the
getSessionmethod signature IF authentication is required for this Use Case operation.execute()MUST NOT be injected into the constructor.getSession- When optional: Public Use Cases (e.g., ,
LoginUser) that allow unauthenticated access MUST NOT includeRegisterUserin params.getSession - When required: Protected Use Cases (e.g., ,
CreateWorkspace) that require user context MUST includeListWorkspacesin params.getSession - Rationale: Passing at invocation time ensures each call evaluates auth dynamically, preventing stale session state.
getSession - Pattern:
async execute({ ...params, getSession }: Params): Promise<Result<T, Errors>>
- When optional: Public Use Cases (e.g.,
-
Whenis included, session verification MUST be the first operation in
getSession. All subsequent logic depends on session validity.execute()
-
如果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>>
- 可选场景:允许未认证访问的公共Use Cases(如
-
如果包含,会话验证必须是
getSession中的第一个操作。后续所有逻辑都依赖于会话的有效性。execute()
Railway-Oriented Error Handling
面向铁路的错误处理
-
MUST return
execute()viaPromise<Result<T, ErrorUnion>>.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 usingbefore proceeding. Every async operation returning
.isErr()MUST be checked for errors.Result
-
必须通过
execute()返回neverthrow。Promise<Result<T, ErrorUnion>> -
绝不能为领域或业务逻辑错误手动抛出异常。所有失败必须包装在中。
err(error) -
在继续处理前,必须使用解构Result值。每个返回
.isErr()的异步操作都必须检查错误。Result
Dependency Injection via Constructor
通过构造函数注入依赖
-
Repositories andMUST be injected in the constructor, not instantiated within
WithTransaction.execute() -
Dependencies MUST be stored asfields and remain immutable.
private readonly
-
仓库和必须注入到构造函数中,而不是在
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: calls
execute()andthis.userRepo.create().this.workspaceRepo.create() - ❌ Wrong: injecting and calling
CreateWorkspace.CreateFolder.execute()
- ✅ Example:
- 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 intomethods within the same class for readability and maintainability.
private
-
每个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.mdWhen 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 . Standard creations without these constraints typically only return
<Dependency>NotFoundError.RepositoryError
- Unique Constraints: e.g., creating a URL with a path that must be unique maps to
- Transactions: Use ONLY when:
withTransaction(async (tx) => { ... })- 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:
- Session verification (if param included) — MUST be first.
getSession - Create domain entities using constructors.
- If multi-step: wrap in transaction, check on each result.
.isErr() - Return created entity or error.
文件:
references/examples/create-example.md适用场景:创建需要跨多个仓库或验证表协调的实体。
特征:
- 接收多个Value Objects作为参数。
- 当数据一致性要求时,在事务边界内协调多个仓库操作。
- 返回领域实体。
- 错误注意事项:向数据库添加新信息的仓库方法必须处理结构约束,并返回特定于领域的数据库错误以及标准:
RepositoryError- 唯一约束:例如,创建路径必须唯一的URL时,对应。
PathAlreadyInUseError - 外键约束:例如,关联不存在的父资源时,对应。 无这些约束的标准创建操作通常仅返回
<Dependency>NotFoundError。RepositoryError
- 唯一约束:例如,创建路径必须唯一的URL时,对应
- 事务:仅在以下场景使用:
withTransaction(async (tx) => { ... })- 同一操作中修改多个仓库(如创建工作区+初始文件夹)
- 部分失败会导致数据处于不一致状态
- 多个操作的原子性是业务需求
- 不推荐用于单表创建(避免不必要的开销)
关键步骤:
- 会话验证(如果包含参数)——必须是第一步。
getSession - 使用构造函数创建领域实体。
- 如果是多步骤:包装在事务中,检查每个结果的。
.isErr() - 返回创建的实体或错误。
Pattern 2: LIST — Query with Pagination & Validation
模式2:LIST——带分页与验证的查询
File:
references/examples/list-example.mdWhen 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) or cursor-based (itemsPerPage,cursor).limit - Receives pagination params as Value Objects (,
PositiveInteger, etc.).Cursor - 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:
- Validate pagination params (e.g., or cursor validity).
itemsPerPage <= maxItemsPerPage - Session verification (if param included).
getSession - Optional: Verify scope hierarchy via (if resource is in a hierarchy).
ensureIsDataConsistent() - Delegate to repository's method.
list()
文件:
references/examples/list-example.md适用场景:检索带有业务规则验证的分页集合。
特征:
- 列表操作必须包含分页,以避免无限制的查询结果。
- 分页可以是基于偏移量的(、
page)或基于游标(itemsPerPage、cursor)。limit - 接收作为Value Objects的分页参数(、
PositiveInteger等)。Cursor - 在调用仓库前验证业务约束(分页限制)。
- 在分层范围内列出资源时,必须先验证父范围的访问权限(如在列出文件夹前验证用户拥有工作区)。
- 返回实体数组:。
Entity[] - 事务:禁止使用。读取操作本身具有一致性。
关键步骤:
- 验证分页参数(如或游标有效性)。
itemsPerPage <= maxItemsPerPage - 会话验证(如果包含参数)。
getSession - 可选:通过验证范围层级(如果资源属于分层结构)。
ensureIsDataConsistent() - 委托给仓库的方法。
list()
Pattern 3: EXISTS — Authorization Check
模式3:EXISTS——授权检查
File:
references/examples/exists-example.mdWhen to use: Checking if a resource exists and belongs to the current user.
Characteristics:
- Minimal logic: auth + repository query.
- Returns wrapped in
boolean.Result - Transactions: MUST NOT be used. Single read operation.
Key steps:
- Session verification (if param included).
getSession - Delegate to repository's method.
exists()
文件:
references/examples/exists-example.md适用场景:检查资源是否存在并属于当前用户。
特征:
- 逻辑极简:认证 + 仓库查询。
- 返回包装在中的
Result。boolean - 事务:禁止使用。仅为单次读取操作。
关键步骤:
- 会话验证(如果包含参数)。
getSession - 委托给仓库的方法。
exists()
Pattern 4: COUNT — Aggregation with Scope Verification
模式4:COUNT——带范围验证的聚合
File:
references/examples/count-example.mdWhen to use: Retrieving aggregate counts for resources within a hierarchical scope.
Characteristics:
- Returns a Value Object representing count (, not primitive
NonNegativeInteger).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:
- Session verification (if param included).
getSession - Verify scope hierarchy via (each repository verifies immediate parent scope).
ensureIsDataConsistent() - If hierarchy valid, delegate to repository's method.
count()
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 (, then
{ workspaceId, userId }).{ folderId, workspaceId }
文件:
references/examples/count-example.md适用场景:检索分层范围内资源的聚合计数。
特征:
- 返回表示计数的Value Object(,而非原始
NonNegativeInteger)。number - 在委托前必须验证范围层级(如用户对工作区的访问权限、工作区对文件夹的访问权限)。
- 在统计子资源前验证父范围存在。
- 事务:禁止使用。仅为只读聚合操作。
关键步骤:
- 会话验证(如果包含参数)。
getSession - 通过验证范围层级(每个仓库验证其直接父范围)。
ensureIsDataConsistent() - 如果层级有效,委托给仓库的方法。
count()
示例层级:User → Workspace → Folder → ShortUrl
- 验证流程:用户是否拥有工作区?→ 工作区是否拥有该文件夹?→ 统计文件夹中的项。
- 每个仓库调用仅传递其直接父范围(,然后是
{ workspaceId, userId })。{ folderId, workspaceId }
Pattern 5: UPDATE — Modifying Resources
模式5:UPDATE——修改资源
File:
references/examples/update-example.mdWhen 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 . Standard updates without constraint changes usually just return
FolderNotFoundError.RepositoryError
- Unique Constraints: e.g., updating a path to one that is already occupied maps to
- Transactions: MUST NOT be used for single-resource updates.
Key steps:
- Session verification (if param included).
getSession - Verify scope hierarchy via (passing all parent IDs).
ensureIsDataConsistent() - Delegate to repository's method.
update()
文件:
references/examples/update-example.md适用场景:修改现有实体字段,不改变所有权逻辑。
特征:
- 必须包含与CREATE/DELETE相同的所有父级ID。
- 接收必填ID和表示待更新字段的可选Value Objects。
- 错误注意事项:修改结构约束的仓库方法必须返回特定于领域的数据库错误以及标准:
RepositoryError- 唯一约束:例如,将路径更新为已占用的路径时,对应。
PathAlreadyInUseError - 外键约束:例如,将短URL移动到已删除的文件夹时,对应。 无约束变更的标准更新操作通常仅返回
FolderNotFoundError。RepositoryError
- 唯一约束:例如,将路径更新为已占用的路径时,对应
- 事务:禁止用于单资源更新。
关键步骤:
- 会话验证(如果包含参数)。
getSession - 通过验证范围层级(传递所有父级ID)。
ensureIsDataConsistent() - 委托给仓库的方法。
update()
Pattern 6: DELETE — Hard vs Soft Deletion
模式6:DELETE——硬删除与软删除
File:
references/examples/delete-example.mdWhen 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., ), you MUST consider foreign key constraints. You must handle child dependencies (either via DB-level
Folderor via a transaction orchestrating multiple repository deletions).ON DELETE CASCADE - Returns .
Result<void, Errors> - Transactions: MUST NOT be used for single-table deletes. MUST be used if manually orchestrating child deletions.
- If deleting an intermediate node (e.g.,
- Soft Delete (Logical removal): Use for high-value data, legal compliance, or recoverable resources.
- MUST NOT use the prefix to avoid ambiguity. Use explicit domain verbs like
Delete,Archive<Entity>, orDeactivate<Entity>.Trash<Entity> - This is technically an operation in the repository.
UPDATE
- MUST NOT use the
Key steps (Hard Delete):
- Session verification (if param included).
getSession - Verify scope hierarchy via (passing all parent IDs).
ensureIsDataConsistent() - Delegate to repository's method.
delete()
文件:
references/examples/delete-example.md适用场景:移除资源或标记为不可用。
特征:
- 硬删除(物理移除):适用于低价值数据或叶子节点(如)。
ShortUrl- 如果删除中间节点(如),必须考虑外键约束。必须处理子依赖(通过数据库级
Folder或协调多个仓库删除的事务)。ON DELETE CASCADE - 返回。
Result<void, Errors> - 事务:禁止用于单表删除。如果手动协调子删除,则必须使用事务。
- 如果删除中间节点(如
- 软删除(逻辑移除):适用于高价值数据、合规要求或可恢复资源。
- 禁止使用前缀以避免歧义。使用明确的领域动词,如
Delete、Archive<Entity>或Deactivate<Entity>。Trash<Entity> - 这在仓库中本质上是操作。
UPDATE
- 禁止使用
硬删除关键步骤:
- 会话验证(如果包含参数)。
getSession - 通过验证范围层级(传递所有父级ID)。
ensureIsDataConsistent() - 委托给仓库的方法。
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:
- : No transaction (single read)
ListWorkspaces - (username + profile in one table): No transaction (single write)
CreateUser - (workspace + default folder): Yes transaction (two tables, atomicity required)
CreateWorkspace
仅在必要时使用事务。滥用会导致性能下降和不必要的复杂度。
必须使用事务的场景:
- ✅ 单个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必须定义两种类型(Params和Errors)。如果实体包含必须隐藏的私有数据,可选择定义**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:
- Scope isolation (Repository): WHERE clauses prevent cross-user/cross-context data access.
- Permission verification (Use Case): Business logic validates that the authenticated user has permission to perform the action.
核心原则:授权分为两个独立层:
- 范围隔离(仓库):WHERE子句防止跨用户/跨上下文数据访问。
- 权限验证(Use Case):业务逻辑验证认证用户是否有权执行操作。
Scope Isolation vs. Permission Verification
范围隔离 vs 权限验证
Scope Isolation (Repository Responsibility):
- Applies WHERE clauses to enforce context boundaries.
- Example: prevents User A from accessing User B's data.
WHERE userId = #{currentUserId} - Handled via repository method parameters: vs.
exists({ workspaceId, userId })for different access levels.exists({ workspaceId })
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子句以强制上下文边界。
- 示例:防止用户A访问用户B的数据。
WHERE userId = #{currentUserId} - 通过仓库方法参数实现:vs
exists({ workspaceId, userId })对应不同访问级别。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:
- → WHERE
workspaceRepository.exists({ workspaceId, userId })id = workspaceId AND userId = ? - → WHERE
folderRepository.exists({ folderId, workspaceId })id = folderId AND workspaceId = ? - → WHERE
shortUrlRepository.exists({ shortUrlId, folderId })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();
}核心要点:每个仓库调用仅传递其直接父范围:
- → WHERE
workspaceRepository.exists({ workspaceId, userId })id = workspaceId AND userId = ? - → WHERE
folderRepository.exists({ folderId, workspaceId })id = folderId AND workspaceId = ? - → WHERE
shortUrlRepository.exists({ shortUrlId, folderId })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 MUST explicitly include the IDs for ALL parent levels in the hierarchy (e.g., , ), 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.
ParamsworkspaceIdfolderIdUser (from session)
└─ Workspace (does user have access?)
└─ Folder (does workspace own this folder?)
└─ ShortUrl (does folder own this short URL?)Example from :
countShortUrls.tstypescript
// 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的必须明确包含层级中所有父级的ID(如、),无论目标实体有多深。Use Case绝不能依赖数据库推断或JOIN父范围;必须将它们作为输入接收以验证完整链。
ParamsworkspaceIdfolderIdUser (来自会话)
└─ Workspace (用户是否有访问权限?)
└─ Folder (工作区是否拥有该文件夹?)
└─ ShortUrl (文件夹是否拥有该短URL?)来自的示例:
countShortUrls.tstypescript
// 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:
- (admin): Calls
DeleteWorkspaceworkspaceRepository.exists({ workspaceId }) - (user): Calls
LeaveWorkspaceworkspaceRepository.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决定使用哪个范围:
- (管理员):调用
DeleteWorkspaceworkspaceRepository.exists({ workspaceId }) - (用户):调用
LeaveWorkspaceworkspaceRepository.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-ormzodneverthrow- Ensure with
WithTransactiontype is available inPgTransaction.src/shared/domain/types/ - Ensure function exists in
withRetry(for resilience patterns).src/shared/ - Ensure type and related auth utilities are available in
GetSession.src/auth/
If these do not exist, read to scaffold the exact implementations.
references/core-utilities.md如果在新项目中使用我们的标准技术栈(、、)设置这些模式:
drizzle-ormzodneverthrow- 确保目录下存在带
src/shared/domain/types/类型的PgTransaction。WithTransaction - 确保目录下存在
src/shared/函数(用于弹性模式)。withRetry - 确保目录下存在
src/auth/类型及相关认证工具。GetSession
如果这些不存在,请阅读以搭建精确的实现。
references/core-utilities.md8. 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 pattern." is non-negotiable.
<Action><Entity>
本技能使用RFC 2119关键字来明确要求:
- MUST / MUST NOT:强制性要求。违反会破坏正确性或安全性。
- SHOULD / SHOULD NOT:强烈建议。只有有文档记录的理由时才可偏离。
- MAY:可选。如有需要允许使用,但非强制。
- 示例:“Use Case名称必须遵循模式”是不可协商的。
<Action><Entity>
9. Anti-Patterns to Avoid
9. 需避免的反模式
| ❌ Anti-Pattern | ✅ Correct Pattern |
|---|---|
| |
| Session check after business logic | Session check as FIRST operation (if included) |
| Mixing primitives with VOs in params | All params are Value Objects |
| Throwing domain errors manually | Wrapping in |
| Transacting single-table writes | Only multi-step/multi-table writes need transactions |
| Multiple public methods per class | Single |
| Direct dependency instantiation | Constructor dependency injection |
| Returning Entities with private data exposed | Return Snapshot type (defined in same file) without private fields |
| Use Case decides all presentation details | Use Case excludes private data only; presentation layer transforms/filters as needed |
| Getting session for unauthenticated Use Cases | Only include |
| Skipping role/permission checks in Use Case | MUST verify user has permission to perform the action (e.g., role-based or capability-based checks) |
| Same Use Case for different access levels | Separate Use Cases: |
| Repository without scope-based WHERE clauses | MUST enforce scope isolation via WHERE: |
| ❌ 反模式 | ✅ 正确模式 |
|---|---|
构造函数中注入 | |
| 业务逻辑之后检查会话 | 会话检查为第一个操作(如果包含) |
| 参数中混合原始类型与VOs | 所有参数均为Value Objects |
| 手动抛出领域错误 | 通过neverthrow包装在 |
| 单表写入使用事务 | 仅多步骤/多表写入需要事务 |
| 每个类有多个公共方法 | 仅单个 |
| 直接实例化依赖 | 构造函数依赖注入 |
| 返回暴露私有数据的实体 | 返回不含私有字段的Snapshot类型(在同一文件中定义) |
| Use Case决定所有展示细节 | Use Case仅排除私有数据;展示层按需转换/过滤 |
| 未认证Use Cases获取会话 | 仅当操作需要认证时才包含 |
| Use Case中跳过角色/权限检查 | 必须验证用户有权执行操作(如基于角色或能力的检查) |
| 同一Use Case对应不同访问级别 | 独立Use Cases: |
| 仓库无基于范围的WHERE子句 | 必须通过WHERE子句强制范围隔离: |