260 lines
7.4 KiB
TypeScript
260 lines
7.4 KiB
TypeScript
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";
|