143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import { mutation, query } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { nowIso } from "./_utils/time";
|
|
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
|
|
|
async function assertWorkspaceMember(ctx: QueryCtx | MutationCtx, userId: string, workspaceId: string) {
|
|
const membership = await ctx.db
|
|
.query("workspace_members")
|
|
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", workspaceId).eq("user_id", userId))
|
|
.first();
|
|
if (!membership) {
|
|
throw new Error("无权访问该工作空间");
|
|
}
|
|
return membership;
|
|
}
|
|
|
|
const displayMode = v.union(v.literal("inline"), v.literal("embed"));
|
|
|
|
export const record = mutation({
|
|
args: {
|
|
userId: v.string(),
|
|
workspaceId: v.string(),
|
|
sourcePageId: v.string(),
|
|
targetPageId: v.string(),
|
|
sourceBlockId: v.union(v.string(), v.null()),
|
|
alias: v.union(v.string(), v.null()),
|
|
displayMode,
|
|
isPreviewable: v.boolean(),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
|
|
|
const ts = nowIso();
|
|
const existing = await ctx.db
|
|
.query("page_references")
|
|
.withIndex("by_unique", (q) =>
|
|
q
|
|
.eq("workspace_id", args.workspaceId)
|
|
.eq("source_page_id", args.sourcePageId)
|
|
.eq("source_block_id", args.sourceBlockId)
|
|
.eq("target_page_id", args.targetPageId)
|
|
.eq("display_mode", args.displayMode),
|
|
)
|
|
.first();
|
|
|
|
if (existing) {
|
|
await ctx.db.patch(existing._id, {
|
|
alias: args.alias ?? null,
|
|
is_previewable: Boolean(args.isPreviewable),
|
|
updated_at: ts,
|
|
});
|
|
return {
|
|
id: existing.id,
|
|
workspace_id: existing.workspace_id,
|
|
source_page_id: existing.source_page_id,
|
|
source_block_id: existing.source_block_id,
|
|
target_page_id: existing.target_page_id,
|
|
alias: args.alias ?? null,
|
|
display_mode: existing.display_mode,
|
|
is_previewable: Boolean(args.isPreviewable),
|
|
created_at: existing.created_at,
|
|
updated_at: ts,
|
|
};
|
|
}
|
|
|
|
// 说明:id 使用可读的组合键,便于调试;并不要求前端依赖该规则。
|
|
const id = `ref:${args.workspaceId}:${args.sourcePageId}:${args.sourceBlockId ?? "page"}:${args.targetPageId}:${args.displayMode}:${ts}`;
|
|
|
|
await ctx.db.insert("page_references", {
|
|
id,
|
|
workspace_id: args.workspaceId,
|
|
source_page_id: args.sourcePageId,
|
|
source_block_id: args.sourceBlockId ?? null,
|
|
target_page_id: args.targetPageId,
|
|
alias: args.alias ?? null,
|
|
display_mode: args.displayMode,
|
|
is_previewable: Boolean(args.isPreviewable),
|
|
created_by: args.userId,
|
|
created_at: ts,
|
|
updated_at: ts,
|
|
});
|
|
|
|
return {
|
|
id,
|
|
workspace_id: args.workspaceId,
|
|
source_page_id: args.sourcePageId,
|
|
source_block_id: args.sourceBlockId ?? null,
|
|
target_page_id: args.targetPageId,
|
|
alias: args.alias ?? null,
|
|
display_mode: args.displayMode,
|
|
is_previewable: Boolean(args.isPreviewable),
|
|
created_at: ts,
|
|
updated_at: ts,
|
|
};
|
|
},
|
|
});
|
|
|
|
export const listBacklinks = query({
|
|
args: {
|
|
userId: v.string(),
|
|
workspaceId: v.string(),
|
|
pageId: v.string(),
|
|
limit: v.optional(v.number()),
|
|
offset: v.optional(v.number()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
|
|
|
const limit = Math.max(1, Math.min(200, Math.floor(args.limit ?? 50)));
|
|
const offset = Math.max(0, Math.floor(args.offset ?? 0));
|
|
|
|
const rows = await ctx.db
|
|
.query("page_references")
|
|
.withIndex("by_workspace_target", (q) => q.eq("workspace_id", args.workspaceId).eq("target_page_id", args.pageId))
|
|
.order("desc")
|
|
.take(limit + offset + 200);
|
|
|
|
const sliced = rows.slice(offset, offset + limit);
|
|
const sourceIds = Array.from(new Set(sliced.map((r) => String((r as any).source_page_id ?? "")).filter(Boolean)));
|
|
|
|
const sourceTitleById = new Map<string, string | null>();
|
|
for (const sid of sourceIds) {
|
|
const doc = await ctx.db.query("documents").withIndex("by_document_id", (q) => q.eq("id", sid)).first();
|
|
sourceTitleById.set(sid, doc ? (doc.title ?? null) : null);
|
|
}
|
|
|
|
return sliced.map((row) => {
|
|
const r = row as any;
|
|
return {
|
|
id: r.id,
|
|
source_page_id: r.source_page_id,
|
|
source_block_id: r.source_block_id ?? null,
|
|
alias: r.alias ?? null,
|
|
display_mode: r.display_mode,
|
|
is_previewable: Boolean(r.is_previewable),
|
|
created_at: r.created_at,
|
|
updated_at: r.updated_at,
|
|
source_title: sourceTitleById.get(String(r.source_page_id ?? "")) ?? null,
|
|
};
|
|
});
|
|
},
|
|
});
|