feat(db,contracts): trash_item migration and file manager contract refinements
- 0024_trash: trash_item table + unique idempotency index on artifact(files_idem) - files.ts: TrashItemRow, delete planId/planExpiresAt, organize group warnings, scrape returns refreshMetadata taskId - registry tests list the new table/row schema Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
673cb3920a
commit
09f6e3a3e5
6 files changed
+98
-2
No files matched your search
@@ -350,6 +350,8 @@ export const FilesOrganizeGroup = z.object({
|
||||
truncated: z.boolean(),
|
||||
recordId: Id.nullable(),
|
||||
taskId: Id.nullable(),
|
||||
/** 暂未生效的逐项覆写(season/episodes/slotId 等待 planImport 扩 fileOverrides) */
|
||||
warnings: z.array(z.string()),
|
||||
/** 该组失败原因(plan_stale / 授权 / H&R 等),其他组不受影响 */
|
||||
error: z.object({ code: z.string(), message: z.string() }).nullable(),
|
||||
});
|
||||
@@ -376,6 +378,8 @@ export const FilesDeleteRequest = z.object({
|
||||
unbind: z.boolean().default(false),
|
||||
/** 永久删除二次确认:必须等于每项文件名(单项时) */
|
||||
confirmName: z.string().max(255).optional(),
|
||||
/** dryRun 返回的 planId;API Key 执行时必填(一次性审批绑定该计划),会话可省略 */
|
||||
planId: Id.optional(),
|
||||
});
|
||||
export type FilesDeleteRequest = z.infer<typeof FilesDeleteRequest>;
|
||||
|
||||
@@ -396,6 +400,25 @@ export const FileDeleteCheck = z.object({
|
||||
});
|
||||
export type FileDeleteCheck = z.infer<typeof FileDeleteCheck>;
|
||||
|
||||
/** trash_item 表行(db-sync 对照用;API 用下方 TrashItem 投影) */
|
||||
export const TrashItemRow = z.object({
|
||||
id: Id,
|
||||
rootId: z.string(),
|
||||
libraryRootId: Id.nullable(),
|
||||
rootHostPath: z.string(),
|
||||
originalPath: z.string(),
|
||||
trashPath: z.string(),
|
||||
isDir: z.boolean(),
|
||||
sizeBytes: NonNegInt,
|
||||
actor: z.string().nullable(),
|
||||
deletedAt: Timestamp,
|
||||
expiresAt: Timestamp,
|
||||
assetSnapshot: JsonObject.nullable(),
|
||||
createdAt: Timestamp,
|
||||
updatedAt: Timestamp,
|
||||
});
|
||||
export type TrashItemRow = z.infer<typeof TrashItemRow>;
|
||||
|
||||
export const TrashItem = z.object({
|
||||
id: Id,
|
||||
rootId: FileRootId,
|
||||
@@ -414,6 +437,9 @@ export type TrashItem = z.infer<typeof TrashItem>;
|
||||
|
||||
export const FilesDeleteResult = z.object({
|
||||
dryRun: z.boolean(),
|
||||
/** dryRun 生成的删除计划(执行时回传;审批绑定) */
|
||||
planId: Id.nullable(),
|
||||
planExpiresAt: Timestamp.nullable(),
|
||||
mode: z.enum(["trash", "permanent"]),
|
||||
checks: z.array(FileDeleteCheck),
|
||||
/** 执行后:进入回收站的项(permanent 为空) */
|
||||
@@ -527,9 +553,11 @@ export const FilesScrapeRequest = z.object({
|
||||
export type FilesScrapeRequest = z.infer<typeof FilesScrapeRequest>;
|
||||
export const FilesScrapeResult = z.object({
|
||||
mediaEntityId: Id,
|
||||
changedFields: z.array(z.string()),
|
||||
/** refreshMetadata 引擎 run id(异步;字段级刷新,用户锁优先,不动路径) */
|
||||
taskId: z.string(),
|
||||
/** 当前被用户锁定、刷新不会改动的字段 */
|
||||
lockedFields: z.array(z.string()),
|
||||
/** NFO/图片投影:W5 实现前恒为 not_implemented */
|
||||
/** NFO/图片投影:W5 实现前请求时为 not_implemented */
|
||||
nfo: z.enum(["written", "skipped", "not_requested", "not_implemented"]),
|
||||
images: z.enum(["written", "skipped", "not_requested", "not_implemented"]),
|
||||
});
|
||||
|
||||
@@ -93,6 +93,8 @@ const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }>
|
||||
BrowserSession: { row: C.BrowserSession },
|
||||
BrowserTab: { row: C.BrowserTab },
|
||||
BrowserControlLease: { row: C.BrowserControlLease },
|
||||
// W5-A 文件管理回收站(0024)
|
||||
TrashItem: { row: C.TrashItemRow },
|
||||
};
|
||||
|
||||
/** 引擎自有表(@umt/tasks 内部使用,不经 API 暴露),不要求 Row schema */
|
||||
|
||||
@@ -33,6 +33,7 @@ const STRING_CANDIDATES = [
|
||||
"catalog.lookup",
|
||||
"测试 Test",
|
||||
"123456",
|
||||
`lib:${UUID}`,
|
||||
];
|
||||
const NUMBER_CANDIDATES = [1, 50, 2024, 0.5, 0, 4096, 128000];
|
||||
|
||||
@@ -88,6 +89,7 @@ const OVERRIDES: Record<string, (v: Record<string, unknown>) => Record<string, u
|
||||
ExternalIdentityCreate: (v) => ({ ...v, contentUnitId: null }),
|
||||
PathBindingCreate: (v) => ({ ...v, bindingKind: "season", seasonNumber: 1 }),
|
||||
OrganizeRequest: (v) => ({ ...v, sourcePaths: undefined }),
|
||||
FilesListQuery: (v) => ({ ...v, filter: "video,unidentified" }),
|
||||
CatalogLookupInput: (v) => v,
|
||||
IdentityResolveInput: (v) => v,
|
||||
AssetLocationCreate: (v) => v,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { dropTables, entityTable, execDdl } from "../src/migration-helpers.ts";
|
||||
|
||||
// W5-A 文件管理(docs/plan/06-file-manager-spec.md §2.3.3):
|
||||
// - trash_item:删除进回收站的记录(原根/原相对路径/回收站内路径/到期时间/关联资产快照);
|
||||
// 还原或清除后删行(历史留在 audit_event)。root_id 为文件管理的根 id(lib:<uuid> / dl:<uuid>:<n>)。
|
||||
// - artifact_files_idem_key:文件管理写操作的幂等键(artifact title='files_idem',content.key)唯一。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
${entityTable(
|
||||
"trash_item",
|
||||
`
|
||||
root_id text not null,
|
||||
library_root_id uuid references library_root (id) on delete set null,
|
||||
root_host_path text not null,
|
||||
original_path text not null,
|
||||
trash_path text not null,
|
||||
is_dir boolean not null default false,
|
||||
size_bytes bigint not null default 0,
|
||||
actor text,
|
||||
deleted_at timestamptz not null default now(),
|
||||
expires_at timestamptz not null,
|
||||
asset_snapshot jsonb,
|
||||
constraint trash_item_trash_path_key unique (trash_path)`,
|
||||
)}
|
||||
create index trash_item_expires_idx on trash_item (expires_at);
|
||||
create index trash_item_root_idx on trash_item (root_id, deleted_at desc);
|
||||
|
||||
create unique index artifact_files_idem_key on artifact ((content->>'key')) where title = 'files_idem';
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, `drop index if exists artifact_files_idem_key;\n${dropTables(["trash_item"])}`);
|
||||
}
|
||||
@@ -1276,6 +1276,23 @@ export interface TitleAliasTable {
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface TrashItemTable {
|
||||
id: Generated<string>;
|
||||
root_id: string;
|
||||
library_root_id: string | null;
|
||||
root_host_path: string;
|
||||
original_path: string;
|
||||
trash_path: string;
|
||||
is_dir: Generated<boolean>;
|
||||
size_bytes: Generated<number>;
|
||||
actor: string | null;
|
||||
deleted_at: GeneratedTimestamptz;
|
||||
expires_at: Timestamptz;
|
||||
asset_snapshot: Json | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
acquisition_plan: AcquisitionPlanTable;
|
||||
acquisition_slot: AcquisitionSlotTable;
|
||||
@@ -1338,6 +1355,7 @@ export interface DB {
|
||||
tasks_op_ledger: TasksOpLedgerTable;
|
||||
tasks_run_meta: TasksRunMetaTable;
|
||||
title_alias: TitleAliasTable;
|
||||
trash_item: TrashItemTable;
|
||||
}
|
||||
|
||||
export type AcquisitionPlanRow = Selectable<AcquisitionPlanTable>;
|
||||
@@ -1523,3 +1541,6 @@ export type TasksRunMetaUpdate = Updateable<TasksRunMetaTable>;
|
||||
export type TitleAliasRow = Selectable<TitleAliasTable>;
|
||||
export type NewTitleAlias = Insertable<TitleAliasTable>;
|
||||
export type TitleAliasUpdate = Updateable<TitleAliasTable>;
|
||||
export type TrashItemRow = Selectable<TrashItemTable>;
|
||||
export type NewTrashItem = Insertable<TrashItemTable>;
|
||||
export type TrashItemUpdate = Updateable<TrashItemTable>;
|
||||
@@ -88,6 +88,8 @@ const EXPECTED_TABLES = [
|
||||
"browser_session",
|
||||
"browser_tab",
|
||||
"browser_control_lease",
|
||||
// W5-A 文件管理回收站(0024)
|
||||
"trash_item",
|
||||
].sort();
|
||||
|
||||
/** 不走 entityTable() 的表:追加型/引擎自有表,没有 uuid id + created_at/updated_at 三件套 */
|
||||
|
||||
Reference in new issue
Block a user