feat(api): album update with independent upload/public switches
PATCH /v1/albums/{albumId}(契约 updateAlbum):部分更新,各开关互相独立;
translations 全量替换(deleteMany+createMany),defaultLocale 可同请求切换但
替换后的翻译集必须覆盖最终 defaultLocale。权限顺序 404(不可见)→403(可见
不可管)与 getAlbum 一致防枚举;合并校验(endAt vs 存量 startAt、单边提交)
在事务内对最终状态裁决。audit album.update 记录 metadata.fields(契约属性序)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// services/api/src/albums/dto.ts —— 相册端点入参 DTO(Task 15)。
|
||||
// 校验约束对齐契约 AlbumCreateRequest / AlbumTranslation:
|
||||
// services/api/src/albums/dto.ts —— 相册端点入参 DTO(Task 15;Task 16 增补更新版)。
|
||||
// 校验约束对齐契约 AlbumCreateRequest / AlbumUpdateRequest / AlbumTranslation:
|
||||
// · translations 至少 1 条(minItems: 1),locale 限 P1a 双语枚举;
|
||||
// · defaultLocale 必须有对应翻译条目(跨字段校验,契约 required 语义);
|
||||
// · endAt 可空(契约 [string,'null']),给值时须晚于 startAt(跨字段校验);
|
||||
@@ -138,3 +138,87 @@ export class AlbumCreateRequestDto {
|
||||
@IsBoolean()
|
||||
originalDownloadEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新版 defaultLocale 覆盖约束(契约 AlbumUpdateRequest):
|
||||
* 仅当同一请求里 defaultLocale 与 translations 同时出现时裁决——
|
||||
* defaultLocale 未提交时本约束放行(服务层对存量 defaultLocale 做合并校验,
|
||||
* 覆盖「只换 translations」与「只切 defaultLocale」两种单边情形)。
|
||||
*/
|
||||
@ValidatorConstraint({ name: 'albumUpdateDefaultLocaleCovered', async: false })
|
||||
class UpdateDefaultLocaleCoveredConstraint
|
||||
implements ValidatorConstraintInterface
|
||||
{
|
||||
validate(value: unknown, args: ValidationArguments): boolean {
|
||||
if (value === undefined || value === null) {
|
||||
return true; // 本次请求不改 defaultLocale。
|
||||
}
|
||||
const { translations } = args.object as { translations?: unknown };
|
||||
if (!Array.isArray(translations) || translations.length === 0) {
|
||||
return true; // translations 不在本次请求里——交服务层对存量行校验。
|
||||
}
|
||||
return translations.some(
|
||||
(entry) =>
|
||||
entry !== null &&
|
||||
typeof entry === 'object' &&
|
||||
(entry as { locale?: unknown }).locale === value,
|
||||
);
|
||||
}
|
||||
|
||||
defaultMessage(): string {
|
||||
return 'translations 必须包含(更新后的)defaultLocale 对应语言的条目';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 契约 AlbumUpdateRequest:全部字段可选 + minProperties:1。
|
||||
* minProperties 无法用 class-validator 内建表达(空体时任何属性级装饰器都
|
||||
* 不会运行),空体拒绝由服务层 update() 入口承担(与创建路径的重复 locale
|
||||
* 检查同一模式);跨字段约束分两层:
|
||||
* · DTO 层——同请求内 endAt>startAt(复用创建版约束:startAt 缺席时放行)、
|
||||
* defaultLocale+translations 同请求时的覆盖关系;
|
||||
* · 服务层——与存量行合并后的时间先后 / defaultLocale 覆盖(单边提交场景:
|
||||
* 只改 endAt 对比存量 startAt、只切 defaultLocale 对比存量翻译行)。
|
||||
*/
|
||||
export class AlbumUpdateRequestDto {
|
||||
@IsOptional()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AlbumTranslationDto)
|
||||
translations?: AlbumTranslationDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(ALBUM_LOCALES)
|
||||
@Validate(UpdateDefaultLocaleCoveredConstraint)
|
||||
defaultLocale?: AlbumLocale;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601({ strict: true })
|
||||
startAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601({ strict: true })
|
||||
@Validate(EndAfterStartConstraint)
|
||||
endAt?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(TIMEZONE_MAX_LENGTH)
|
||||
timezone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
uploadEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
publicEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
downloadEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
originalDownloadEnabled?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user