261 lines
8.3 KiB
TypeScript
261 lines
8.3 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;
|
|
}
|
|
|
|
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,
|
|
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()),
|
|
},
|
|
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("群组不属于当前工作空间");
|
|
|
|
const ts = nowIso();
|
|
const includeDescendants = Boolean(args.includeDescendants);
|
|
|
|
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,
|
|
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,
|
|
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;
|
|
},
|
|
});
|