feat: land page aggregate and phase7 document ai mainline
- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
@@ -9,9 +9,10 @@ import {
|
||||
documentBridgeErrorResponse,
|
||||
} from "@/lib/documents/bridge";
|
||||
import {
|
||||
executeMetadataBridgeCommand,
|
||||
type DocumentOptionsUpdatePayload,
|
||||
} from "@/lib/documents/metadata-command-adapter";
|
||||
import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter";
|
||||
import { PAGE_COMMAND_NAMES } from "@/lib/documents/page-command-contract";
|
||||
|
||||
type OptionsPayload = {
|
||||
documentId: string;
|
||||
@@ -31,7 +32,7 @@ export async function POST(request: Request) {
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
});
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "documents.options.update",
|
||||
name: PAGE_COMMAND_NAMES.updateLayout,
|
||||
payload: {
|
||||
documentId: normalizedDocumentId,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
@@ -43,7 +44,7 @@ export async function POST(request: Request) {
|
||||
pageId: normalizedDocumentId,
|
||||
},
|
||||
});
|
||||
const result = await executeMetadataBridgeCommand({
|
||||
const result = await executePageWriteBridgeCommand({
|
||||
context: bridgeContext,
|
||||
envelope,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import {
|
||||
assertDocumentId,
|
||||
documentBridgeErrorResponse,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { loadPageAggregate } from "@/lib/documents/page-aggregate-loader";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const documentId = assertDocumentId(url.searchParams.get("documentId"));
|
||||
const workspaceId = url.searchParams.get("workspaceId")?.trim() || null;
|
||||
const loaded = await loadPageAggregate({
|
||||
request,
|
||||
documentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!loaded) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "页面不存在",
|
||||
meta: {
|
||||
requestId: "unknown",
|
||||
traceId: "unknown",
|
||||
queryName: "documents.page.get",
|
||||
},
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
page: loaded.page,
|
||||
meta: loaded.bridge,
|
||||
});
|
||||
} catch (error) {
|
||||
return documentBridgeErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
||||
}
|
||||
@@ -4,6 +4,28 @@ vi.mock("@/lib/convex/enabled", () => ({
|
||||
isConvexEnabled: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/authContext", () => ({
|
||||
HttpError: class HttpError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
},
|
||||
requireAuthContext: vi.fn(async () => ({
|
||||
userId: "user_1",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api-utils", () => ({
|
||||
apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({
|
||||
message,
|
||||
status,
|
||||
details,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/page-command-adapter", () => ({
|
||||
executeDocumentCreateChildBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
executeDocumentEmbedBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
@@ -12,11 +34,52 @@ vi.mock("@/lib/documents/page-command-adapter", () => ({
|
||||
executeDocumentPurgeBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/page-write-command-adapter", () => ({
|
||||
executePageWriteBridgeCommand: vi.fn(async () => ({
|
||||
requestId: "req_1",
|
||||
traceId: "trace_1",
|
||||
commandId: "cmd_1",
|
||||
commandName: "page.command",
|
||||
revision: 1,
|
||||
conflictDetectionKey: "doc_1:1",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/page-aggregate-loader", () => ({
|
||||
loadPageAggregate: vi.fn(async () => ({
|
||||
page: {
|
||||
identity: { documentId: "doc_1", workspaceId: "ws_1" },
|
||||
head: {
|
||||
title: "页面标题",
|
||||
updatedAt: null,
|
||||
permissions: {
|
||||
readOnly: false,
|
||||
disableDownload: false,
|
||||
disableCopy: false,
|
||||
},
|
||||
},
|
||||
layout: { pageOptions: { wideLayout: false } },
|
||||
body: { content: null, revision: 0, conflictDetectionKey: "doc_1:0" },
|
||||
tree: { pageSubtree: null },
|
||||
stats: null,
|
||||
},
|
||||
bridge: {
|
||||
requestId: "req_1",
|
||||
traceId: "trace_1",
|
||||
queryName: "documents.page.get",
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { POST as postCreateChild } from "@/app/api/documents/create-child/route";
|
||||
import { POST as postEmbed } from "@/app/api/documents/embed/route";
|
||||
import { POST as postTemplate } from "@/app/api/documents/template/route";
|
||||
import { POST as postEmptyTrash } from "@/app/api/documents/empty-trash/route";
|
||||
import { POST as postPurge } from "@/app/api/documents/purge/route";
|
||||
import { POST as postTitle } from "@/app/api/documents/title/route";
|
||||
import { POST as postOptions } from "@/app/api/documents/options/route";
|
||||
import { POST as postSave } from "@/app/api/documents/save/route";
|
||||
import { GET as getPage } from "@/app/api/documents/page/route";
|
||||
import {
|
||||
executeDocumentCreateChildBridgeCommand,
|
||||
executeDocumentEmbedBridgeCommand,
|
||||
@@ -24,6 +87,8 @@ import {
|
||||
executeDocumentEmptyTrashBridgeCommand,
|
||||
executeDocumentPurgeBridgeCommand,
|
||||
} from "@/lib/documents/page-command-adapter";
|
||||
import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter";
|
||||
import { loadPageAggregate } from "@/lib/documents/page-aggregate-loader";
|
||||
|
||||
describe("documents route adapters", () => {
|
||||
it("creates child route delegates to unified adapter", async () => {
|
||||
@@ -65,4 +130,46 @@ describe("documents route adapters", () => {
|
||||
}));
|
||||
expect(executeDocumentPurgeBridgeCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("page route delegates to unified aggregate loader", async () => {
|
||||
const response = await getPage(
|
||||
new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1"),
|
||||
);
|
||||
const payload = await response.json() as {
|
||||
page: { identity: { documentId: string } };
|
||||
meta: { queryName: string };
|
||||
};
|
||||
|
||||
expect(loadPageAggregate).toHaveBeenCalled();
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload.page.identity.documentId).toBe("doc_1");
|
||||
expect(payload.meta.queryName).toBe("documents.page.get");
|
||||
});
|
||||
|
||||
it("title route delegates to unified page write adapter", async () => {
|
||||
await postTitle(new Request("http://localhost/api/documents/title", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题" }),
|
||||
}));
|
||||
|
||||
expect(executePageWriteBridgeCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("options route delegates to unified page write adapter", async () => {
|
||||
await postOptions(new Request("http://localhost/api/documents/options", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", options: { wideLayout: true } }),
|
||||
}));
|
||||
|
||||
expect(executePageWriteBridgeCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("save route delegates to unified page write adapter", async () => {
|
||||
await postSave(new Request("http://localhost/api/documents/save", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", content: [] }),
|
||||
}));
|
||||
|
||||
expect(executePageWriteBridgeCommand).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
buildDocumentCommandEnvelope,
|
||||
documentBridgeErrorResponse,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { executeSaveBridgeCommand } from "@/lib/documents/save-command-adapter";
|
||||
import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter";
|
||||
import {
|
||||
buildDocumentSavePayload,
|
||||
type DocumentSavePayload,
|
||||
} from "@/lib/documents/save-contract";
|
||||
import { PAGE_COMMAND_NAMES } from "@/lib/documents/page-command-contract";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
@@ -31,7 +32,7 @@ export async function POST(request: Request) {
|
||||
const normalizedWorkspaceId = payload.workspaceId;
|
||||
const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId: normalizedWorkspaceId });
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "documents.save",
|
||||
name: PAGE_COMMAND_NAMES.saveBody,
|
||||
payload: payload satisfies DocumentSavePayload,
|
||||
context: bridgeContext,
|
||||
target: {
|
||||
@@ -39,7 +40,7 @@ export async function POST(request: Request) {
|
||||
pageId: normalizedDocumentId,
|
||||
},
|
||||
});
|
||||
const result = await executeSaveBridgeCommand({
|
||||
const result = await executePageWriteBridgeCommand({
|
||||
context: bridgeContext,
|
||||
envelope,
|
||||
});
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
documentBridgeErrorResponse,
|
||||
} from "@/lib/documents/bridge";
|
||||
import {
|
||||
executeMetadataBridgeCommand,
|
||||
type DocumentTitleUpdatePayload,
|
||||
} from "@/lib/documents/metadata-command-adapter";
|
||||
import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter";
|
||||
import { PAGE_COMMAND_NAMES } from "@/lib/documents/page-command-contract";
|
||||
|
||||
interface RenamePayload {
|
||||
documentId: string;
|
||||
@@ -27,7 +28,7 @@ export async function POST(request: Request) {
|
||||
const normalizedWorkspaceId = workspaceId?.trim() || null;
|
||||
const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId: normalizedWorkspaceId });
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "documents.title.update",
|
||||
name: PAGE_COMMAND_NAMES.updateTitle,
|
||||
payload: {
|
||||
documentId: normalizedDocumentId,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
@@ -39,7 +40,7 @@ export async function POST(request: Request) {
|
||||
pageId: normalizedDocumentId,
|
||||
},
|
||||
});
|
||||
const result = await executeMetadataBridgeCommand({
|
||||
const result = await executePageWriteBridgeCommand({
|
||||
context: bridgeContext,
|
||||
envelope,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user