forked from EduCraft/curriculum-project-hub
fix(filelib): 恢复撞名不再死锁,自动改名「(已恢复)」(ADR-0033)
- restore 前查活跃兄弟:撞名则恢复为「原名(已恢复[/ N])」(截断计入 128 长度预算),同事务落审计并记 renamedFrom;API 返回最终名 - BinView toast 提示改名;测试补撞名/二次恢复用例
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import { FileLibError } from "./model.js";
|
||||
import { FileLibError, nameKey, NODE_NAME_MAX_LENGTH } from "./model.js";
|
||||
import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js";
|
||||
import type { GroupResolver } from "./groupResolver.js";
|
||||
import type { FileLibActor } from "./treeService.js";
|
||||
@@ -100,7 +100,7 @@ async function requireBinEntry(
|
||||
actor: FileLibActor,
|
||||
groupIds: readonly string[],
|
||||
nodeId: string,
|
||||
): Promise<{ readonly id: string; readonly kind: "FOLDER" | "PROJECT"; readonly name: string; readonly pathIds: string }> {
|
||||
): Promise<{ readonly id: string; readonly parentId: string | null; readonly kind: "FOLDER" | "PROJECT"; readonly name: string; readonly pathIds: string }> {
|
||||
const node = await tx.fileLibNode.findFirst({
|
||||
where: { id: nodeId, organizationId: deps.organizationId, deletedAt: { not: null } },
|
||||
});
|
||||
@@ -108,15 +108,51 @@ async function requireBinEntry(
|
||||
if (!(await canSeeEntry(tx, deps, actor, groupIds, node.id))) {
|
||||
throw new FileLibError(404, "node_not_found", "node not found");
|
||||
}
|
||||
return { id: node.id, kind: node.kind, name: node.name, pathIds: node.pathIds };
|
||||
return { id: node.id, parentId: node.parentId, kind: node.kind, name: node.name, pathIds: node.pathIds };
|
||||
}
|
||||
|
||||
/** 恢复:只清本节点 deletedAt(子树随之可见);落 restore 审计。 */
|
||||
export async function restoreBinEntry(deps: BinDeps, actor: FileLibActor, nodeId: string): Promise<void> {
|
||||
export interface RestoreResult {
|
||||
readonly name: string;
|
||||
readonly renamedFrom?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复:只清本节点 deletedAt(子树随之可见);落 restore 审计。
|
||||
* ADR-0033:与活跃兄弟撞名时不失败,自动改成「原名(已恢复[/ N])」——
|
||||
* 恢复的意义就是找回,撞名死锁不是保护;审计 detail 记 renamedFrom。
|
||||
*/
|
||||
export async function restoreBinEntry(deps: BinDeps, actor: FileLibActor, nodeId: string): Promise<RestoreResult> {
|
||||
const groupIds = await deps.groupResolver.resolveMemberGroupIds(actor.userId);
|
||||
await deps.prisma.$transaction(async (tx) => {
|
||||
return deps.prisma.$transaction(async (tx) => {
|
||||
const node = await requireBinEntry(tx as PrismaClient, deps, actor, groupIds, nodeId);
|
||||
await tx.fileLibNode.update({ where: { id: node.id }, data: { deletedAt: null } });
|
||||
|
||||
const siblings = await tx.fileLibNode.findMany({
|
||||
where: {
|
||||
organizationId: deps.organizationId,
|
||||
parentId: node.parentId,
|
||||
deletedAt: null,
|
||||
id: { not: node.id },
|
||||
},
|
||||
select: { nameLower: true },
|
||||
});
|
||||
const taken = new Set(siblings.map((s) => s.nameLower));
|
||||
|
||||
let name = node.name;
|
||||
let renamedFrom: string | undefined;
|
||||
if (taken.has(nameKey(name))) {
|
||||
renamedFrom = node.name;
|
||||
let n = 1;
|
||||
do {
|
||||
const suffix = n === 1 ? "(已恢复)" : `(已恢复 ${n})`;
|
||||
name = node.name.slice(0, NODE_NAME_MAX_LENGTH - suffix.length) + suffix;
|
||||
n += 1;
|
||||
} while (taken.has(nameKey(name)));
|
||||
}
|
||||
|
||||
await tx.fileLibNode.update({
|
||||
where: { id: node.id },
|
||||
data: { deletedAt: null, name, nameLower: nameKey(name) },
|
||||
});
|
||||
await writeFileLibAudit(tx, {
|
||||
action: node.kind === "PROJECT"
|
||||
? FILE_LIB_AUDIT_ACTIONS.projectRestore
|
||||
@@ -126,8 +162,9 @@ export async function restoreBinEntry(deps: BinDeps, actor: FileLibActor, nodeId
|
||||
objectType: node.kind === "PROJECT" ? "project" : "folder",
|
||||
objectId: node.id,
|
||||
objectPath: node.pathIds,
|
||||
detail: { name: node.name },
|
||||
detail: { name, ...(renamedFrom !== undefined ? { renamedFrom } : {}) },
|
||||
});
|
||||
return { name, renamedFrom };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user