feat: cut over rust web main shell
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type CopyTreeItem = {
|
||||
documentId: string;
|
||||
recursive: boolean;
|
||||
};
|
||||
|
||||
type CopyTreePayload = {
|
||||
items?: CopyTreeItem[] | null;
|
||||
targetParentId?: string | null;
|
||||
};
|
||||
|
||||
type TreeCopyResponse = {
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
result?: {
|
||||
items?: Array<{
|
||||
oldId: string;
|
||||
newId: string;
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isConvexEnabled()) {
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = (await request.json()) as CopyTreePayload;
|
||||
const normalizedItems = (payload.items ?? [])
|
||||
.filter((item) => item?.documentId)
|
||||
.map((item) => ({
|
||||
documentId: item.documentId.trim(),
|
||||
recursive: Boolean(item.recursive),
|
||||
}))
|
||||
.filter((item) => item.documentId.length > 0);
|
||||
|
||||
if (normalizedItems.length === 0) {
|
||||
return NextResponse.json({ error: "items 为空" }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: "copy",
|
||||
targetParentId:
|
||||
typeof payload.targetParentId === "string" ? payload.targetParentId.trim() || null : null,
|
||||
items: normalizedItems,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = (await response.json().catch(() => null)) as
|
||||
| TreeCopyResponse
|
||||
| { error?: string }
|
||||
| null;
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
result && typeof result === "object" && "error" in result && typeof result.error === "string"
|
||||
? result.error
|
||||
: "复制页面失败,请稍后再试",
|
||||
},
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
items: result?.result?.items ?? [],
|
||||
meta: {
|
||||
requestId: result?.requestId,
|
||||
traceId: result?.traceId,
|
||||
commandName: "tree.subtree.copy",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "复制页面失败,请稍后再试",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -1,83 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type TreeCreateResponse = {
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
result?: {
|
||||
documentId?: string;
|
||||
parentId?: string | null;
|
||||
title?: string | null;
|
||||
sortOrder?: number | null;
|
||||
workspaceId?: string | null;
|
||||
execution?: {
|
||||
access_scope?: "private" | "shared" | "public";
|
||||
is_template?: boolean;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isConvexEnabled()) {
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { parentId }: { parentId?: string | null } = await request.json();
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: "create",
|
||||
parentId: typeof parentId === "string" ? parentId.trim() || null : null,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as TreeCreateResponse | { error?: string } | null;
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
||||
? payload.error
|
||||
: "创建页面失败,请稍后再试",
|
||||
},
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
id: payload?.result?.documentId ?? "",
|
||||
title: payload?.result?.title ?? "无标题",
|
||||
parent_id: payload?.result?.parentId ?? null,
|
||||
sort_order: payload?.result?.sortOrder ?? null,
|
||||
workspace_id: payload?.result?.workspaceId ?? undefined,
|
||||
access_scope: payload?.result?.execution?.access_scope ?? "private",
|
||||
is_template: payload?.result?.execution?.is_template ?? false,
|
||||
created_at: payload?.result?.execution?.created_at ?? null,
|
||||
updated_at:
|
||||
payload?.result?.execution?.updated_at ??
|
||||
payload?.result?.execution?.created_at ??
|
||||
null,
|
||||
meta: {
|
||||
requestId: payload?.requestId,
|
||||
traceId: payload?.traceId,
|
||||
commandName: "tree.node.create",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "创建页面失败,请稍后再试",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -1,75 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type TreeArchiveResponse = {
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
};
|
||||
|
||||
type DeletePayload = {
|
||||
documentId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isConvexEnabled()) {
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { documentId, workspaceId }: DeletePayload = await request.json();
|
||||
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
|
||||
if (!normalizedDocumentId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: "archive",
|
||||
documentId: normalizedDocumentId,
|
||||
workspaceId: typeof workspaceId === "string" ? workspaceId.trim() || null : null,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as
|
||||
| TreeArchiveResponse
|
||||
| { error?: string }
|
||||
| null;
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
||||
? payload.error
|
||||
: "删除失败,请稍后再试",
|
||||
},
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
meta: {
|
||||
requestId: payload?.requestId,
|
||||
traceId: payload?.traceId,
|
||||
commandName: "tree.node.archive",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "删除失败,请稍后再试",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -1,74 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type TreeEmbedResponse = {
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
};
|
||||
|
||||
type EmbedPayload = {
|
||||
sourceId?: string | null;
|
||||
targetId?: string | null;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isConvexEnabled()) {
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { sourceId, targetId }: EmbedPayload = await request.json();
|
||||
const normalizedSourceId = typeof sourceId === "string" ? sourceId.trim() : "";
|
||||
const normalizedTargetId = typeof targetId === "string" ? targetId.trim() : "";
|
||||
if (!normalizedSourceId || !normalizedTargetId) {
|
||||
return NextResponse.json({ error: "缺少 sourceId 或 targetId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: "embed",
|
||||
sourceId: normalizedSourceId,
|
||||
targetId: normalizedTargetId,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as
|
||||
| TreeEmbedResponse
|
||||
| { error?: string }
|
||||
| null;
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
||||
? payload.error
|
||||
: "嵌入失败,请稍后再试",
|
||||
},
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
meta: {
|
||||
requestId: payload?.requestId,
|
||||
traceId: payload?.traceId,
|
||||
commandName: "tree.node.embed",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "嵌入失败,请稍后再试",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -1,79 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type TreeMoveResponse = {
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
result?: {
|
||||
documentId?: string;
|
||||
parentId?: string | null;
|
||||
sortOrder?: number | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type MovePayload = {
|
||||
documentId?: string | null;
|
||||
parentId?: string | null;
|
||||
position?: number | null;
|
||||
workspaceId?: string | null;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isConvexEnabled()) {
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { documentId, parentId = null, position, workspaceId }: MovePayload = await request.json();
|
||||
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
|
||||
if (!normalizedDocumentId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: "move",
|
||||
documentId: normalizedDocumentId,
|
||||
parentId: typeof parentId === "string" ? parentId.trim() || null : null,
|
||||
sortOrder: typeof position === "number" && Number.isFinite(position) ? Math.floor(position) : 0,
|
||||
workspaceId: typeof workspaceId === "string" ? workspaceId.trim() || null : null,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as TreeMoveResponse | { error?: string } | null;
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
||||
? payload.error
|
||||
: "移动失败,请稍后再试",
|
||||
},
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
meta: {
|
||||
requestId: payload?.requestId,
|
||||
traceId: payload?.traceId,
|
||||
commandName: "tree.subtree.move",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "移动失败,请稍后再试",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -1,77 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type TreePurgeResponse = {
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
result?: {
|
||||
purged?: boolean;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type PurgePayload = {
|
||||
documentId?: string | null;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isConvexEnabled()) {
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { documentId }: PurgePayload = await request.json();
|
||||
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
|
||||
if (!normalizedDocumentId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: "purge",
|
||||
documentId: normalizedDocumentId,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as
|
||||
| TreePurgeResponse
|
||||
| { error?: string }
|
||||
| null;
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
||||
? payload.error
|
||||
: "彻底删除失败,请稍后再试",
|
||||
},
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
purged: payload?.result?.purged ?? true,
|
||||
meta: {
|
||||
requestId: payload?.requestId,
|
||||
traceId: payload?.traceId,
|
||||
commandName: "tree.node.purge",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "彻底删除失败,请稍后再试",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -1,75 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type TreeRestoreResponse = {
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
};
|
||||
|
||||
type RestorePayload = {
|
||||
documentId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isConvexEnabled()) {
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { documentId, workspaceId }: RestorePayload = await request.json();
|
||||
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
|
||||
if (!normalizedDocumentId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: "restore",
|
||||
documentId: normalizedDocumentId,
|
||||
workspaceId: typeof workspaceId === "string" ? workspaceId.trim() || null : null,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as
|
||||
| TreeRestoreResponse
|
||||
| { error?: string }
|
||||
| null;
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
||||
? payload.error
|
||||
: "恢复失败,请稍后再试",
|
||||
},
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
meta: {
|
||||
requestId: payload?.requestId,
|
||||
traceId: payload?.traceId,
|
||||
commandName: "tree.node.restore",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "恢复失败,请稍后再试",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -214,7 +214,11 @@ describe("documents route adapters", () => {
|
||||
|
||||
const response = await postTitle(new Request("http://localhost/api/documents/title", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: {
|
||||
"authorization": "Bearer test-token",
|
||||
"content-type": "application/json",
|
||||
"cookie": "convex-auth=test-cookie",
|
||||
},
|
||||
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题" }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
@@ -235,6 +239,15 @@ describe("documents route adapters", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const renameCall = fetchMock.mock.calls.find(([, init]) => {
|
||||
if (!init || typeof init.body !== "string") {
|
||||
return false;
|
||||
}
|
||||
return init.body.includes('"action":"rename"');
|
||||
});
|
||||
const forwardedHeaders = renameCall?.[1]?.headers as Headers;
|
||||
expect(forwardedHeaders.get("authorization")).toBe("Bearer test-token");
|
||||
expect(forwardedHeaders.get("cookie")).toBe("convex-auth=test-cookie");
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.meta.commandName).toBe("tree.node.rename");
|
||||
});
|
||||
|
||||
@@ -35,11 +35,20 @@ export async function POST(request: Request) {
|
||||
|
||||
if (commandName !== PAGE_COMMAND_NAMES.updateTitle) {
|
||||
const upstreamUrl = new URL("/api/tree/commands", request.url);
|
||||
const upstreamHeaders = new Headers({
|
||||
"content-type": "application/json",
|
||||
});
|
||||
const authorization = request.headers.get("authorization");
|
||||
const cookie = request.headers.get("cookie");
|
||||
if (authorization) {
|
||||
upstreamHeaders.set("authorization", authorization);
|
||||
}
|
||||
if (cookie) {
|
||||
upstreamHeaders.set("cookie", cookie);
|
||||
}
|
||||
const response = await fetch(upstreamUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
headers: upstreamHeaders,
|
||||
body: JSON.stringify({
|
||||
action: "rename",
|
||||
documentId: normalizedDocumentId,
|
||||
|
||||
Reference in New Issue
Block a user