Files
mnote/wolai-frontend/src/app/api/tree/commands/route.ts
T

260 lines
7.4 KiB
TypeScript
Raw Normal View History

2026-04-24 06:10:18 +08:00
import { randomUUID } from "node:crypto";
import { NextResponse } from "next/server";
import { api } from "@/lib/convex/api";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import {
assertDocumentId,
assertTitle,
buildDocumentBridgeContext,
buildDocumentCommandEnvelope,
documentBridgeErrorResponse,
} from "@/lib/documents/bridge";
import { ensureDocumentScaffold } from "@/lib/documents/page-lifecycle-side-effects";
import {
executeRustBridgeMutationTransport,
resolveRustBridgeCommandPlan,
} from "@/lib/documents/rust-runtime";
type TreeCommandPayload = {
action?: "create" | "move" | "rename";
workspaceId?: string | null;
documentId?: string | null;
parentId?: string | null;
title?: string | null;
accessScope?: "private" | "shared" | "public" | null;
content?: unknown;
sortOrder?: number | null;
};
function trimOrNull(value: unknown) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function normalizeTitle(value: unknown) {
const title = typeof value === "string" ? value.trim() : "";
return title || "无标题";
}
function normalizeSortOrder(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
}
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const payload = (await request.json()) as TreeCommandPayload;
switch (payload.action) {
case "create":
return handleCreate(request, payload);
case "move":
return handleMove(request, payload);
case "rename":
return handleRename(request, payload);
default:
return NextResponse.json({ error: "不支持的 tree action" }, { status: 400 });
}
} catch (error) {
return documentBridgeErrorResponse(error);
}
}
async function handleCreate(request: Request, payload: TreeCommandPayload) {
const { auth, client } = await getAuthedConvexClient();
const parentId = trimOrNull(payload.parentId);
const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
fallbackName: auth.email ?? auth.name ?? "我的空间",
workspaceIdIfCreate: randomUUID(),
});
let workspaceId: string | null = trimOrNull(payload.workspaceId);
let accessScope: "private" | "shared" | "public" = "private";
if (parentId) {
const parentDoc = await client.query(api.documents.getMeta, { id: parentId });
if (!parentDoc) {
return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 });
}
workspaceId = trimOrNull(parentDoc.workspace_id);
accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope;
} else if (!workspaceId) {
workspaceId = trimOrNull(workspaceBootstrap.activeWorkspaceId);
}
if (!workspaceId) {
return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
}
const documentId = trimOrNull(payload.documentId) ?? randomUUID();
const title = normalizeTitle(payload.title);
const context = await buildDocumentBridgeContext({
request,
workspaceId,
});
const envelope = buildDocumentCommandEnvelope({
name: "tree.node.create",
payload: {
documentId,
workspaceId,
parentId,
title,
accessScope: trimOrNull(payload.accessScope) ?? accessScope,
content: Array.isArray(payload.content) ? payload.content : [],
},
context,
target: {
workspaceId,
pageId: documentId,
},
reason: "tree-route create",
refs: ["next-tree-route"],
});
const plan = await resolveRustBridgeCommandPlan({
context,
envelope,
});
const result = await executeRustBridgeMutationTransport<{
id: string;
title: string | null;
parent_id: string | null;
sort_order: number | null;
workspace_id: string;
access_scope: "private" | "shared" | "public";
is_template: boolean;
created_at: string;
updated_at: string;
}>({
client,
plan,
});
await ensureDocumentScaffold(result.id, result.title ?? title);
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "create",
workspaceId,
documentId: result.id,
parentId: result.parent_id ?? parentId,
title: result.title ?? title,
sortOrder: result.sort_order ?? null,
updatedAt: result.updated_at ?? null,
execution: result,
},
});
}
async function handleMove(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const documentId = assertDocumentId(payload.documentId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
const parentId = trimOrNull(payload.parentId);
const sortOrder = normalizeSortOrder(payload.sortOrder);
const context = await buildDocumentBridgeContext({
request,
workspaceId,
});
const envelope = buildDocumentCommandEnvelope({
name: "tree.subtree.move",
payload: {
documentId,
parentId,
sortOrder,
},
context,
target: {
workspaceId,
pageId: documentId,
},
reason: "tree-route move",
refs: ["next-tree-route"],
});
const plan = await resolveRustBridgeCommandPlan({
context,
envelope,
});
const result = await executeRustBridgeMutationTransport<{ ok?: boolean; updated_at?: string | null }>({
client,
plan,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "move",
workspaceId,
documentId,
parentId,
sortOrder,
updatedAt: result?.updated_at ?? null,
execution: result ?? null,
},
});
}
async function handleRename(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const documentId = assertDocumentId(payload.documentId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
const title = assertTitle(payload.title ?? null);
const context = await buildDocumentBridgeContext({
request,
workspaceId,
});
const envelope = buildDocumentCommandEnvelope({
name: "tree.node.rename",
payload: {
documentId,
workspaceId,
title,
},
context,
target: {
workspaceId,
pageId: documentId,
},
reason: "tree-route rename",
refs: ["next-tree-route"],
});
const plan = await resolveRustBridgeCommandPlan({
context,
envelope,
});
const result = await executeRustBridgeMutationTransport<{ ok?: boolean; updated_at?: string | null }>({
client,
plan,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "rename",
workspaceId,
documentId,
title,
updatedAt: result?.updated_at ?? null,
execution: result ?? null,
},
});
}
export const runtime = "nodejs";