Files
photo-live/services/api/src/albums/support.ts
T
浅念 e04e1f3bdf feat(api): members and categories management
成员(/v1/albums/{albumId}/members):owner/管理员邀请已开通同事为协作者
(重复 409 MEMBER_CONFLICT、owner 目标 400、未开通/停用账号 400)、成员列表
((addedAt,id) DESC 键集分页)、移除协作者(幂等 204;成员行是唯一权限来源,
删除行即失去可见性 A02;owner 不可移除 403)。分类(/albums/{albumId}/categories
与 /categories/{categoryId}):创建/部分更新/删除,translations 全量替换,
相册详情按 sortOrder 升序带出;404 统一 ALBUM_NOT_FOUND(分类不存在与相册
不可见同码)。审计 album.member.add/remove、album.category.add/update/remove
(resourceId=albumId,metadata 记目标 id/字段)。共用错误/P2002 判定抽至
support.ts(修复 meta.target 为 DB 约束名时的误判)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 22:16:16 +08:00

58 lines
2.0 KiB
TypeScript

// services/api/src/albums/support.ts —— 相册域服务共用小工具(Task 15 起自
// albums.service.ts 抽出,Task 17 起 members/categories 服务同用)。
import { HttpStatus } from '@nestjs/common';
import { ErrorCodes } from '@photo/contracts';
import { ApiException } from '../api-exception';
/** 统一 404:不可见(含不存在)同码防 id 枚举(契约 albums/categories 404 语义)。 */
export function albumNotFound(): ApiException {
return new ApiException(
ErrorCodes.ALBUM_NOT_FOUND,
'相册不存在或不可见',
HttpStatus.NOT_FOUND,
);
}
/** 统一 403:可见但无管理权限(协作者视角)。 */
export function albumForbidden(message = '仅创建者或管理员可操作'): ApiException {
return new ApiException(ErrorCodes.AUTH_FORBIDDEN, message, HttpStatus.FORBIDDEN);
}
/** 统一 400(契约 VALIDATION_FAILED)。 */
export function validationFailed(message: string): ApiException {
return new ApiException(
ErrorCodes.VALIDATION_FAILED,
message,
HttpStatus.BAD_REQUEST,
);
}
/**
* P2002 是否命中指定唯一索引。uniqueName 用 Prisma 唯一选择器名(单列
* 'publicToken' / 复合 'albumId_userId')。meta.target 两种形态都兼容:
* 字段数组(部分引擎/版本)或 DB 约束名字符串(MySQL 实测形如
* 'albums_publicToken_key' / 'album_members_albumId_userId_key'——
* 表名前缀 + 选择器名 + _key 后缀,两种后缀都匹配)。
*/
export function isUniqueViolationOn(err: unknown, uniqueName: string): boolean {
if (typeof err !== 'object' || err === null) {
return false;
}
const { code, meta } = err as { code?: unknown; meta?: { target?: unknown } };
if (code !== 'P2002') {
return false;
}
const target = meta?.target;
if (Array.isArray(target)) {
return target.map(String).includes(uniqueName);
}
if (typeof target === 'string') {
return (
target === uniqueName ||
target.endsWith(`${uniqueName}_key`) ||
target.endsWith(`_${uniqueName}_key`)
);
}
return false;
}