Files
mnote/wolai-frontend/convex/documentGroupShares.ts
T
2026-01-24 12:32:51 +08:00

281 lines
9.1 KiB
TypeScript

import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { nowIso } from "./_utils/time";
type Permission = "read" | "edit";
async function requireUserId(ctx: any): Promise<string> {
const userId = await getAuthUserId(ctx);
if (userId === null) throw new Error("未登录");
return String(userId);
}
async function requireOwnedDocument(ctx: any, documentId: string, userId: string) {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", documentId))
.first();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== userId) throw new Error("无权限");
return doc;
}
async function requireGroup(ctx: any, groupId: string) {
const group = await ctx.db
.query("groups")
.withIndex("by_group_id", (q: any) => q.eq("id", groupId))
.first();
if (!group) throw new Error("群组不存在");
return group;
}
async function requireGroupMember(ctx: any, groupId: string, userId: string) {
const membership = await ctx.db
.query("group_members")
.withIndex("by_group_user", (q: any) => q.eq("group_id", groupId).eq("user_id", userId))
.first();
if (!membership) throw new Error("无权限(你不在该群组内)");
return membership;
}
export const listByDocument = query({
args: { documentId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const shares = await ctx.db
.query("document_group_shares")
.withIndex("by_document", (q: any) => q.eq("document_id", doc.id))
.collect();
const result = [];
for (const share of shares) {
const group = await ctx.db
.query("groups")
.withIndex("by_group_id", (q: any) => q.eq("id", share.group_id))
.first();
const perms = await ctx.db
.query("document_group_user_permissions")
.withIndex("by_workspace_group", (q: any) =>
q.eq("workspace_id", doc.workspace_id).eq("group_id", share.group_id),
)
.collect();
const editableUserIds = perms
.filter((p: any) => p.document_id === doc.id && p.permission === "edit")
.map((p: any) => String(p.user_id));
result.push({
groupId: share.group_id,
groupName: group?.name ?? null,
includeDescendants: share.include_descendants,
disableDownload: Boolean((share as any).disable_download),
disableCopy: Boolean((share as any).disable_copy),
editableUserIds,
updatedAt: share.updated_at,
});
}
return result;
},
});
export const upsert = mutation({
args: {
documentId: v.string(),
groupId: v.string(),
includeDescendants: v.optional(v.boolean()),
editableUserIds: v.array(v.string()),
disableDownload: v.optional(v.boolean()),
disableCopy: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const group = await requireGroup(ctx, args.groupId);
if (group.workspace_id !== doc.workspace_id) throw new Error("群组不属于当前工作空间");
await requireGroupMember(ctx, args.groupId, userId);
const ts = nowIso();
const includeDescendants = Boolean(args.includeDescendants);
const disableDownload = Boolean(args.disableDownload);
const disableCopy = Boolean(args.disableCopy);
const existingShare = await ctx.db
.query("document_group_shares")
.withIndex("by_doc_group", (q: any) => q.eq("document_id", doc.id).eq("group_id", args.groupId))
.first();
if (existingShare) {
await ctx.db.patch(existingShare._id, {
include_descendants: includeDescendants,
disable_download: disableDownload,
disable_copy: disableCopy,
updated_at: ts,
});
} else {
await ctx.db.insert("document_group_shares", {
workspace_id: doc.workspace_id,
group_id: args.groupId,
document_id: doc.id,
include_descendants: includeDescendants,
disable_download: disableDownload,
disable_copy: disableCopy,
created_by: userId,
created_at: ts,
updated_at: ts,
});
}
// 更新成员权限:默认只读,仅对可编辑用户写入 override(edit)。
const existingPerms = await ctx.db
.query("document_group_user_permissions")
.withIndex("by_workspace_group", (q: any) =>
q.eq("workspace_id", doc.workspace_id).eq("group_id", args.groupId),
)
.collect();
const targetSet = new Set(args.editableUserIds.map(String));
for (const p of existingPerms) {
if (p.document_id !== doc.id) continue;
const uid = String(p.user_id);
if (!targetSet.has(uid)) {
await ctx.db.delete(p._id);
}
}
for (const uid of targetSet) {
const existing = existingPerms.find(
(p: any) => p.document_id === doc.id && String(p.user_id) === uid,
);
if (existing) {
if (existing.permission !== "edit") {
await ctx.db.patch(existing._id, { permission: "edit" as Permission, updated_at: ts });
}
} else {
await ctx.db.insert("document_group_user_permissions", {
workspace_id: doc.workspace_id,
group_id: args.groupId,
document_id: doc.id,
user_id: uid,
permission: "edit" as Permission,
created_at: ts,
updated_at: ts,
});
}
}
await ctx.db.patch(doc._id, { updated_at: ts });
return { ok: true };
},
});
export const remove = mutation({
args: { documentId: v.string(), groupId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const existingShare = await ctx.db
.query("document_group_shares")
.withIndex("by_doc_group", (q: any) => q.eq("document_id", doc.id).eq("group_id", args.groupId))
.first();
if (existingShare) {
await ctx.db.delete(existingShare._id);
}
const perms = await ctx.db
.query("document_group_user_permissions")
.withIndex("by_workspace_group", (q: any) =>
q.eq("workspace_id", doc.workspace_id).eq("group_id", args.groupId),
)
.collect();
for (const p of perms) {
if (p.document_id !== doc.id) continue;
await ctx.db.delete(p._id);
}
await ctx.db.patch(doc._id, { updated_at: nowIso() });
return { ok: true };
},
});
export const listPublicByWorkspace = query({
args: { workspaceId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const member = await ctx.db
.query("workspace_members")
.withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
.first();
if (!member) throw new Error("无权限");
const myGroups = await ctx.db
.query("group_members")
.withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
.collect();
const myGroupIds = new Set<string>(myGroups.map((g: any) => String(g.group_id)));
const myCreatedShares = await ctx.db
.query("document_group_shares")
.withIndex("by_workspace_created_by", (q: any) => q.eq("workspace_id", args.workspaceId).eq("created_by", userId))
.collect();
const visibleGroupIds = new Set<string>(myGroupIds);
myCreatedShares.forEach((s: any) => visibleGroupIds.add(String(s.group_id)));
const groups = await ctx.db
.query("groups")
.withIndex("by_workspace", (q: any) => q.eq("workspace_id", args.workspaceId))
.collect();
const groupNameById = new Map<string, string>();
for (const g of groups) {
if (visibleGroupIds.has(String(g.id))) {
groupNameById.set(String(g.id), String(g.name ?? ""));
}
}
const sharesByGroup = [];
for (const groupId of visibleGroupIds) {
const rows = await ctx.db
.query("document_group_shares")
.withIndex("by_workspace_group", (q: any) => q.eq("workspace_id", args.workspaceId).eq("group_id", groupId))
.collect();
if (rows.length === 0) continue;
const docMap = new Map<string, { includeDescendants: boolean }>();
for (const r of rows) {
const existing = docMap.get(r.document_id);
if (!existing) {
docMap.set(r.document_id, { includeDescendants: Boolean(r.include_descendants) });
} else {
existing.includeDescendants = existing.includeDescendants || Boolean(r.include_descendants);
}
}
sharesByGroup.push({
groupId,
groupName: groupNameById.get(groupId) ?? null,
documents: Array.from(docMap.entries()).map(([documentId, v]) => ({
documentId,
includeDescendants: v.includeDescendants,
})),
});
}
// 按群组名称排序,便于折叠展示
sharesByGroup.sort((a: any, b: any) => String(a.groupName ?? "").localeCompare(String(b.groupName ?? "")));
return sharesByGroup;
},
});