232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
import { randomUUID } from "crypto";
|
|
import { NextResponse } from "next/server";
|
|
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
|
import { ensureDefaultWorkspace, resolveActiveWorkspaceId } from "@/lib/workspaces";
|
|
import type { Json } from "@/types/supabase";
|
|
import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content";
|
|
import path from "path";
|
|
import { promises as fs } from "fs";
|
|
import { getDocumentsBaseDir } from "@/lib/server/local-paths";
|
|
import { isConvexEnabled } from "@/lib/convex/enabled";
|
|
import { getConvexHttpClient } from "@/lib/convex/server";
|
|
import { api } from "@/lib/convex/api";
|
|
import { requireAuthContext } from "@/lib/auth/authContext";
|
|
|
|
const documentsBaseDir = getDocumentsBaseDir();
|
|
|
|
async function ensureDocumentScaffold(id: string, title: string | null) {
|
|
const folder = path.join(documentsBaseDir, id);
|
|
const indexFile = path.join(folder, "index.md");
|
|
await fs.mkdir(folder, { recursive: true });
|
|
try {
|
|
await fs.access(indexFile);
|
|
} catch {
|
|
const safeTitle = title && title.trim() ? title.trim() : "无标题";
|
|
const content = `# ${safeTitle}\n`;
|
|
await fs.writeFile(indexFile, content, "utf8");
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
if (isConvexEnabled()) {
|
|
return await handleCreateRequestConvex(request);
|
|
}
|
|
return await handleCreateRequest(request);
|
|
} catch (error) {
|
|
console.error("创建页面失败", error);
|
|
const message = error instanceof Error ? error.message : "创建页面失败,请稍后再试";
|
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
async function handleCreateRequestConvex(request: Request) {
|
|
const auth = requireAuthContext();
|
|
const client = getConvexHttpClient();
|
|
|
|
const { parentId }: { parentId?: string | null } = await request.json();
|
|
|
|
const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
|
|
userId: auth.userId,
|
|
fallbackName: auth.email ?? auth.name ?? "我的空间",
|
|
workspaceIdIfCreate: randomUUID(),
|
|
});
|
|
|
|
let workspaceId: string | null = null;
|
|
let parentContent: Json | null = null;
|
|
let accessScope: "private" | "shared" | "public" = "private";
|
|
|
|
if (parentId) {
|
|
const parentDoc = await client.query(api.documents.getMeta, {
|
|
userId: auth.userId,
|
|
id: parentId,
|
|
});
|
|
|
|
if (!parentDoc) {
|
|
return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 });
|
|
}
|
|
|
|
workspaceId = parentDoc.workspace_id;
|
|
accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope;
|
|
const parentContentRes = await client.query(api.documents.getContent, { userId: auth.userId, id: parentId });
|
|
parentContent = (parentContentRes?.content as Json | null) ?? null;
|
|
} else {
|
|
workspaceId = workspaceBootstrap.activeWorkspaceId || null;
|
|
}
|
|
|
|
if (!workspaceId) {
|
|
return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
|
|
}
|
|
|
|
const id = randomUUID();
|
|
const data = await client.mutation(api.documents.create, {
|
|
userId: auth.userId,
|
|
id,
|
|
workspaceId,
|
|
parentId: parentId ?? null,
|
|
title: "无标题",
|
|
accessScope,
|
|
content: [],
|
|
});
|
|
|
|
// 为文件树创建本地目录和 index.md
|
|
if (data?.id) {
|
|
await ensureDocumentScaffold(data.id, data.title ?? "无标题");
|
|
}
|
|
|
|
if (parentId && data) {
|
|
const existingBlocks = extractBlocksFromContent(parentContent);
|
|
const pageReferenceBlock = {
|
|
id: randomUUID(),
|
|
type: "pageReference",
|
|
props: {
|
|
pageId: data.id,
|
|
title: data.title ?? "无标题",
|
|
},
|
|
};
|
|
const nextBlocks = [...existingBlocks, pageReferenceBlock as Json];
|
|
const payload = composeContentWithBlocks(parentContent, nextBlocks);
|
|
|
|
await client.mutation(api.documents.updateContent, {
|
|
userId: auth.userId,
|
|
id: parentId,
|
|
content: payload,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json(data);
|
|
}
|
|
|
|
async function handleCreateRequest(request: Request) {
|
|
const supabase = await createSupabaseRouteClient();
|
|
const {
|
|
data: { session },
|
|
} = await supabase.auth.getSession();
|
|
|
|
if (!session) {
|
|
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const { parentId }: { parentId?: string | null } = await request.json();
|
|
|
|
await ensureDefaultWorkspace(supabase, session.user.id, session.user.email ?? "我的空间");
|
|
|
|
let workspaceId: string | null = null;
|
|
let parentContent: Json | null = null;
|
|
let accessScope: "private" | "shared" | "public" = "private";
|
|
|
|
if (parentId) {
|
|
const { data: parentDoc, error: parentError } = await supabase
|
|
.from("documents")
|
|
.select("workspace_id,access_scope,content,user_id")
|
|
.eq("id", parentId)
|
|
.eq("user_id", session.user.id)
|
|
.single();
|
|
|
|
if (parentError || !parentDoc) {
|
|
return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 });
|
|
}
|
|
workspaceId = parentDoc.workspace_id;
|
|
accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope;
|
|
parentContent = parentDoc.content;
|
|
} else {
|
|
workspaceId = await resolveActiveWorkspaceId(supabase, session.user.id);
|
|
if (!workspaceId) {
|
|
return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
|
|
}
|
|
}
|
|
|
|
if (!workspaceId) {
|
|
return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
|
|
}
|
|
|
|
const siblingQuery = supabase
|
|
.from("documents")
|
|
.select("id", { head: true, count: "exact" })
|
|
.eq("workspace_id", workspaceId);
|
|
|
|
if (parentId) {
|
|
siblingQuery.eq("parent_id", parentId);
|
|
} else {
|
|
siblingQuery.is("parent_id", null);
|
|
}
|
|
|
|
const { count: rawSiblingCount, error: countError } = await siblingQuery;
|
|
const siblingCount = rawSiblingCount ?? 0;
|
|
|
|
if (countError) {
|
|
return NextResponse.json({ error: countError.message }, { status: 500 });
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("documents")
|
|
.insert({
|
|
user_id: session.user.id,
|
|
parent_id: parentId ?? null,
|
|
workspace_id: workspaceId,
|
|
title: "无标题",
|
|
content: { blocks: [] },
|
|
access_scope: accessScope,
|
|
sort_order: siblingCount,
|
|
})
|
|
.select(
|
|
"id,title,parent_id,sort_order,is_starred,created_at,updated_at,workspace_id,access_scope,is_template",
|
|
)
|
|
.single();
|
|
|
|
if (error) {
|
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
|
}
|
|
|
|
// 为文件树创建本地目录和 index.md
|
|
if (data?.id) {
|
|
await ensureDocumentScaffold(data.id, data.title ?? "无标题");
|
|
}
|
|
|
|
if (parentId && data) {
|
|
const existingBlocks = extractBlocksFromContent(parentContent);
|
|
const pageReferenceBlock = {
|
|
id: randomUUID(),
|
|
type: "pageReference",
|
|
props: {
|
|
pageId: data.id,
|
|
title: data.title ?? "无标题",
|
|
},
|
|
};
|
|
const nextBlocks = [...existingBlocks, pageReferenceBlock as Json];
|
|
const payload = composeContentWithBlocks(parentContent, nextBlocks);
|
|
const timestamp = new Date().toISOString();
|
|
const { error: parentUpdateError } = await supabase
|
|
.from("documents")
|
|
.update({ content: payload, updated_at: timestamp })
|
|
.eq("id", parentId)
|
|
.eq("user_id", session.user.id);
|
|
|
|
if (parentUpdateError) {
|
|
return NextResponse.json({ error: parentUpdateError.message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
return NextResponse.json(data);
|
|
}
|