0.3.1 UI修复
This commit is contained in:
@@ -1,11 +1,54 @@
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { internalQuery, mutation, query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import { nowIso } from "./_utils/time";
|
||||
import { enqueueIngestDocumentJob } from "./_utils/ingestJobs";
|
||||
|
||||
const accessScope = v.union(v.literal("private"), v.literal("shared"), v.literal("public"));
|
||||
|
||||
async function requireUserId(ctx: any): Promise<string> {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (userId === null) {
|
||||
throw new Error("未登录");
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export const getMeta = query({
|
||||
args: { id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) return null;
|
||||
if (doc.user_id !== userId) return null;
|
||||
return {
|
||||
id: doc.id,
|
||||
user_id: doc.user_id,
|
||||
workspace_id: doc.workspace_id,
|
||||
access_scope: doc.access_scope,
|
||||
title: doc.title ?? null,
|
||||
parent_id: doc.parent_id ?? null,
|
||||
created_at: doc.created_at,
|
||||
updated_at: doc.updated_at ?? null,
|
||||
wide_layout: doc.wide_layout ?? null,
|
||||
use_small_text: doc.use_small_text ?? null,
|
||||
show_heading_numbers: doc.show_heading_numbers ?? null,
|
||||
show_toc: doc.show_toc ?? null,
|
||||
show_structure: doc.show_structure ?? null,
|
||||
protect_editing: doc.protect_editing ?? null,
|
||||
show_word_count: doc.show_word_count ?? null,
|
||||
word_count: doc.word_count ?? null,
|
||||
character_count: doc.character_count ?? null,
|
||||
block_count: doc.block_count ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const getMetaForIngest = internalQuery({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const doc = await ctx.db
|
||||
@@ -38,6 +81,21 @@ export const getMeta = query({
|
||||
});
|
||||
|
||||
export const getContent = query({
|
||||
args: { id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) return null;
|
||||
if (doc.user_id !== userId) return null;
|
||||
return { content: doc.content ?? null };
|
||||
},
|
||||
});
|
||||
|
||||
export const getContentForIngest = internalQuery({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const doc = await ctx.db
|
||||
@@ -51,8 +109,10 @@ export const getContent = query({
|
||||
});
|
||||
|
||||
export const listByWorkspace = query({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
args: { workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const docs = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
||||
@@ -60,7 +120,7 @@ export const listByWorkspace = query({
|
||||
|
||||
// 说明:阶段 4 先不做垃圾桶(deleted_at != null),因此这里直接过滤。
|
||||
return docs
|
||||
.filter((d) => d.user_id === args.userId)
|
||||
.filter((d) => d.user_id === userId)
|
||||
.filter((d) => d.deleted_at == null)
|
||||
.map((d) => ({
|
||||
access_scope: d.access_scope,
|
||||
@@ -78,15 +138,17 @@ export const listByWorkspace = query({
|
||||
});
|
||||
|
||||
export const listTrashedByWorkspace = query({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
args: { workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const docs = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
||||
.collect();
|
||||
|
||||
return docs
|
||||
.filter((d) => d.user_id === args.userId)
|
||||
.filter((d) => d.user_id === userId)
|
||||
.filter((d) => d.deleted_at != null)
|
||||
.sort((a, b) => (b.deleted_at ?? "").localeCompare(a.deleted_at ?? ""))
|
||||
.slice(0, 100)
|
||||
@@ -102,7 +164,6 @@ export const listTrashedByWorkspace = query({
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
id: v.string(),
|
||||
workspaceId: v.string(),
|
||||
parentId: v.union(v.string(), v.null()),
|
||||
@@ -111,6 +172,8 @@ export const create = mutation({
|
||||
content: v.optional(v.any()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const siblings = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace_parent", (q) => q.eq("workspace_id", args.workspaceId).eq("parent_id", args.parentId))
|
||||
@@ -124,7 +187,7 @@ export const create = mutation({
|
||||
|
||||
await ctx.db.insert("documents", {
|
||||
id: args.id,
|
||||
user_id: args.userId,
|
||||
user_id: userId,
|
||||
workspace_id: args.workspaceId,
|
||||
parent_id: args.parentId,
|
||||
title,
|
||||
@@ -166,33 +229,37 @@ export const create = mutation({
|
||||
});
|
||||
|
||||
export const updateContent = mutation({
|
||||
args: { userId: v.string(), id: v.string(), content: v.any() },
|
||||
args: { id: v.string(), content: v.any() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { content: args.content, updated_at: ts });
|
||||
|
||||
// 说明:在 Convex 模式下,把“自动入库/LightRAG 触发”迁到 Convex jobs/actions。
|
||||
// 说明:在 Convex 模式下,把"自动入库/LightRAG 触发"迁到 Convex jobs/actions。
|
||||
// 采用 debounce,避免频繁保存时触发过多任务。
|
||||
await enqueueIngestDocumentJob(ctx, { userId: args.userId, documentId: args.id, debounceMs: 1500 });
|
||||
await enqueueIngestDocumentJob(ctx, { userId, documentId: args.id, debounceMs: 1500 });
|
||||
return { ok: true, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
export const updateTitle = mutation({
|
||||
args: { userId: v.string(), id: v.string(), title: v.union(v.string(), v.null()) },
|
||||
args: { id: v.string(), title: v.union(v.string(), v.null()) },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { title: args.title, updated_at: ts });
|
||||
return { ok: true, updated_at: ts };
|
||||
@@ -201,18 +268,19 @@ export const updateTitle = mutation({
|
||||
|
||||
export const move = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
id: v.string(),
|
||||
parentId: v.union(v.string(), v.null()),
|
||||
sortOrder: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, {
|
||||
parent_id: args.parentId,
|
||||
@@ -224,29 +292,33 @@ export const move = mutation({
|
||||
});
|
||||
|
||||
export const softDelete = mutation({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
args: { id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { deleted_at: ts, deleted_by: args.userId, updated_at: ts });
|
||||
await ctx.db.patch(doc._id, { deleted_at: ts, deleted_by: userId, updated_at: ts });
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const restore = mutation({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
args: { id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, {
|
||||
deleted_at: null,
|
||||
@@ -260,26 +332,30 @@ export const restore = mutation({
|
||||
});
|
||||
|
||||
export const purge = mutation({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
args: { id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
await ctx.db.delete(doc._id);
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const emptyTrashByWorkspace = mutation({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
args: { workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
// 说明:阶段 4/5 先用“membership 存在即可”的规则,避免引入复杂权限模型。
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
// 说明:阶段 4/5 先用"membership 存在即可"的规则,避免引入复杂权限模型。
|
||||
const membership = await ctx.db
|
||||
.query("workspace_members")
|
||||
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", args.userId))
|
||||
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
|
||||
.first();
|
||||
|
||||
if (!membership) {
|
||||
@@ -291,7 +367,7 @@ export const emptyTrashByWorkspace = mutation({
|
||||
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
||||
.collect();
|
||||
|
||||
const toDelete = docs.filter((d) => d.user_id === args.userId && d.deleted_at != null);
|
||||
const toDelete = docs.filter((d) => d.user_id === userId && d.deleted_at != null);
|
||||
for (const d of toDelete) {
|
||||
await ctx.db.delete(d._id);
|
||||
}
|
||||
@@ -302,7 +378,6 @@ export const emptyTrashByWorkspace = mutation({
|
||||
|
||||
export const updateOptions = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
id: v.string(),
|
||||
options: v.object({
|
||||
wideLayout: v.optional(v.boolean()),
|
||||
@@ -315,12 +390,14 @@ export const updateOptions = mutation({
|
||||
}),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (typeof args.options.wideLayout === "boolean") patch.wide_layout = args.options.wideLayout;
|
||||
@@ -344,19 +421,20 @@ export const updateOptions = mutation({
|
||||
|
||||
export const updateStats = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
id: v.string(),
|
||||
wordCount: v.number(),
|
||||
characterCount: v.number(),
|
||||
blockCount: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.user_id !== args.userId) throw new Error("无权限");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, {
|
||||
word_count: args.wordCount,
|
||||
@@ -370,18 +448,19 @@ export const updateStats = mutation({
|
||||
|
||||
export const duplicate = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
sourceId: v.string(),
|
||||
newId: v.string(),
|
||||
title: v.optional(v.union(v.string(), v.null())),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const source = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_document_id", (q) => q.eq("id", args.sourceId))
|
||||
.first();
|
||||
if (!source) throw new Error("页面不存在或无权限访问");
|
||||
if (source.user_id !== args.userId) throw new Error("页面不存在或无权限访问");
|
||||
if (source.user_id !== userId) throw new Error("页面不存在或无权限访问");
|
||||
|
||||
const siblings = await ctx.db
|
||||
.query("documents")
|
||||
@@ -397,7 +476,7 @@ export const duplicate = mutation({
|
||||
|
||||
await ctx.db.insert("documents", {
|
||||
id: args.newId,
|
||||
user_id: args.userId,
|
||||
user_id: userId,
|
||||
workspace_id: source.workspace_id,
|
||||
parent_id: source.parent_id,
|
||||
title,
|
||||
@@ -438,15 +517,17 @@ export const duplicate = mutation({
|
||||
});
|
||||
|
||||
export const listAllForCopy = query({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
args: { workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const docs = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
||||
.collect();
|
||||
|
||||
return docs
|
||||
.filter((d) => d.user_id === args.userId)
|
||||
.filter((d) => d.user_id === userId)
|
||||
.filter((d) => d.deleted_at == null)
|
||||
.map((d) => ({
|
||||
id: d.id,
|
||||
|
||||
@@ -122,8 +122,8 @@ export const run = internalAction({
|
||||
if (!documentId) throw new Error("缺少 documentId");
|
||||
|
||||
const [meta, contentRes] = await Promise.all([
|
||||
ctx.runQuery(api.documents.getMeta, { userId: job.user_id, id: documentId }),
|
||||
ctx.runQuery(api.documents.getContent, { userId: job.user_id, id: documentId }),
|
||||
ctx.runQuery(internal.documents.getMetaForIngest, { userId: job.user_id, id: documentId }),
|
||||
ctx.runQuery(internal.documents.getContentForIngest, { userId: job.user_id, id: documentId }),
|
||||
]);
|
||||
|
||||
if (!meta) throw new Error("页面不存在或无权限");
|
||||
@@ -147,8 +147,8 @@ export const run = internalAction({
|
||||
if (!mindmapId) throw new Error("缺少 mindmapId");
|
||||
|
||||
const [docMeta, mindmapRes] = await Promise.all([
|
||||
ctx.runQuery(api.documents.getMeta, { userId: job.user_id, id: docId }),
|
||||
ctx.runQuery(api.mindmaps.get, { userId: job.user_id, docId, mindmapId }),
|
||||
ctx.runQuery(internal.documents.getMetaForIngest, { userId: job.user_id, id: docId }),
|
||||
ctx.runQuery(internal.mindmaps.getForIngest, { userId: job.user_id, docId, mindmapId }),
|
||||
]);
|
||||
|
||||
if (!docMeta) throw new Error("页面不存在或无权限");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { internalQuery, mutation, query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import { nowIso } from "./_utils/time";
|
||||
import { enqueueIngestMindmapJob } from "./_utils/ingestJobs";
|
||||
|
||||
@@ -8,6 +9,14 @@ const defaultMindmapData = {
|
||||
children: [],
|
||||
};
|
||||
|
||||
async function requireUserId(ctx: any): Promise<string> {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (userId === null) {
|
||||
throw new Error("未登录");
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
function normalizeMindmapId(docId: string, mindmapId: string): string {
|
||||
const raw = String(mindmapId ?? "").trim();
|
||||
if (raw) return raw;
|
||||
@@ -30,9 +39,11 @@ async function requireOwnedDocument(ctx: any, userId: string, docId: string) {
|
||||
}
|
||||
|
||||
export const get = query({
|
||||
args: { userId: v.string(), docId: v.string(), mindmapId: v.string() },
|
||||
args: { docId: v.string(), mindmapId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const doc = await requireOwnedDocument(ctx, args.userId, args.docId);
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await requireOwnedDocument(ctx, userId, args.docId);
|
||||
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
||||
|
||||
const row = await ctx.db
|
||||
@@ -71,16 +82,58 @@ export const get = query({
|
||||
},
|
||||
});
|
||||
|
||||
export const getForIngest = internalQuery({
|
||||
args: { userId: v.string(), docId: v.string(), mindmapId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const doc = await requireOwnedDocument(ctx, args.userId, args.docId);
|
||||
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
||||
|
||||
const row = await ctx.db
|
||||
.query("mindmaps")
|
||||
.withIndex("by_doc_mindmap", (q) => q.eq("document_id", args.docId).eq("mindmap_id", mindmapId))
|
||||
.first();
|
||||
|
||||
if (!row || row.deleted_at != null) {
|
||||
return {
|
||||
ok: true,
|
||||
data: defaultMindmapData,
|
||||
meta: {
|
||||
workspace_id: doc.workspace_id,
|
||||
document_id: args.docId,
|
||||
mindmap_id: mindmapId,
|
||||
exists: false,
|
||||
deleted_at: row?.deleted_at ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data: row.data ?? defaultMindmapData,
|
||||
meta: {
|
||||
workspace_id: row.workspace_id,
|
||||
document_id: row.document_id,
|
||||
mindmap_id: row.mindmap_id,
|
||||
exists: true,
|
||||
deleted_at: row.deleted_at,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const put = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
docId: v.string(),
|
||||
mindmapId: v.string(),
|
||||
data: v.any(),
|
||||
createOnly: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const doc = await requireOwnedDocument(ctx, args.userId, args.docId);
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const doc = await requireOwnedDocument(ctx, userId, args.docId);
|
||||
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
||||
|
||||
const existing = await ctx.db
|
||||
@@ -102,13 +155,13 @@ export const put = mutation({
|
||||
deleted_at: null,
|
||||
deleted_by: null,
|
||||
});
|
||||
await enqueueIngestMindmapJob(ctx, { userId: args.userId, docId: args.docId, mindmapId, debounceMs: 1500 });
|
||||
await enqueueIngestMindmapJob(ctx, { userId, docId: args.docId, mindmapId, debounceMs: 1500 });
|
||||
return { ok: true, created: false, skipped: false, updated_at: ts };
|
||||
}
|
||||
|
||||
await ctx.db.insert("mindmaps", {
|
||||
id: `${args.docId}:${mindmapId}`,
|
||||
user_id: args.userId,
|
||||
user_id: userId,
|
||||
workspace_id: doc.workspace_id,
|
||||
document_id: args.docId,
|
||||
mindmap_id: mindmapId,
|
||||
@@ -119,15 +172,17 @@ export const put = mutation({
|
||||
deleted_by: null,
|
||||
});
|
||||
|
||||
await enqueueIngestMindmapJob(ctx, { userId: args.userId, docId: args.docId, mindmapId, debounceMs: 1500 });
|
||||
await enqueueIngestMindmapJob(ctx, { userId, docId: args.docId, mindmapId, debounceMs: 1500 });
|
||||
return { ok: true, created: true, skipped: false, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
export const softDelete = mutation({
|
||||
args: { userId: v.string(), docId: v.string(), mindmapId: v.string() },
|
||||
args: { docId: v.string(), mindmapId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
await requireOwnedDocument(ctx, args.userId, args.docId);
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
await requireOwnedDocument(ctx, userId, args.docId);
|
||||
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
||||
|
||||
const existing = await ctx.db
|
||||
@@ -141,15 +196,17 @@ export const softDelete = mutation({
|
||||
}
|
||||
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(existing._id, { deleted_at: ts, deleted_by: args.userId, updated_at: ts });
|
||||
await ctx.db.patch(existing._id, { deleted_at: ts, deleted_by: userId, updated_at: ts });
|
||||
return { ok: true, moved: 1, deleted_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
export const restore = mutation({
|
||||
args: { userId: v.string(), docId: v.string(), mindmapId: v.string() },
|
||||
args: { docId: v.string(), mindmapId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
await requireOwnedDocument(ctx, args.userId, args.docId);
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
await requireOwnedDocument(ctx, userId, args.docId);
|
||||
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
||||
|
||||
const existing = await ctx.db
|
||||
@@ -168,9 +225,11 @@ export const restore = mutation({
|
||||
});
|
||||
|
||||
export const purge = mutation({
|
||||
args: { userId: v.string(), docId: v.string(), mindmapId: v.string() },
|
||||
args: { docId: v.string(), mindmapId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
await requireOwnedDocument(ctx, args.userId, args.docId);
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
await requireOwnedDocument(ctx, userId, args.docId);
|
||||
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
||||
|
||||
const existing = await ctx.db
|
||||
@@ -188,12 +247,14 @@ export const purge = mutation({
|
||||
});
|
||||
|
||||
export const listByWorkspace = query({
|
||||
args: { userId: v.string(), workspaceId: v.string(), includeDeleted: v.optional(v.boolean()) },
|
||||
args: { workspaceId: v.string(), includeDeleted: v.optional(v.boolean()) },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
// 说明:阶段 6 先按 membership 存在即可,避免引入复杂权限模型。
|
||||
const membership = await ctx.db
|
||||
.query("workspace_members")
|
||||
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", args.userId))
|
||||
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
|
||||
.first();
|
||||
if (!membership) {
|
||||
throw new Error("无权操作该工作空间");
|
||||
@@ -207,7 +268,7 @@ export const listByWorkspace = query({
|
||||
const includeDeleted = Boolean(args.includeDeleted);
|
||||
|
||||
return rows
|
||||
.filter((r) => r.user_id === args.userId)
|
||||
.filter((r) => r.user_id === userId)
|
||||
.filter((r) => (includeDeleted ? true : r.deleted_at == null))
|
||||
.sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""))
|
||||
.map((r) => ({
|
||||
@@ -225,10 +286,12 @@ export const listByWorkspace = query({
|
||||
});
|
||||
|
||||
export const copyByDocument = mutation({
|
||||
args: { userId: v.string(), sourceDocId: v.string(), targetDocId: v.string() },
|
||||
args: { sourceDocId: v.string(), targetDocId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const sourceDoc = await requireOwnedDocument(ctx, args.userId, args.sourceDocId);
|
||||
const targetDoc = await requireOwnedDocument(ctx, args.userId, args.targetDocId);
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const sourceDoc = await requireOwnedDocument(ctx, userId, args.sourceDocId);
|
||||
const targetDoc = await requireOwnedDocument(ctx, userId, args.targetDocId);
|
||||
|
||||
// 说明:同一工作空间复制最常见;若跨工作空间复制,这里仍允许,但 mindmaps 会落到目标页面的 workspace_id。
|
||||
const sourceRows = await ctx.db
|
||||
@@ -237,7 +300,7 @@ export const copyByDocument = mutation({
|
||||
.collect();
|
||||
|
||||
const sourceMindmaps = sourceRows
|
||||
.filter((r) => r.user_id === args.userId)
|
||||
.filter((r) => r.user_id === userId)
|
||||
.filter((r) => r.document_id === args.sourceDocId)
|
||||
.filter((r) => r.deleted_at == null);
|
||||
|
||||
@@ -253,7 +316,7 @@ export const copyByDocument = mutation({
|
||||
|
||||
await ctx.db.insert("mindmaps", {
|
||||
id: `${args.targetDocId}:${r.mindmap_id}`,
|
||||
user_id: args.userId,
|
||||
user_id: userId,
|
||||
workspace_id: targetDoc.workspace_id,
|
||||
document_id: args.targetDocId,
|
||||
mindmap_id: r.mindmap_id,
|
||||
@@ -271,11 +334,13 @@ export const copyByDocument = mutation({
|
||||
});
|
||||
|
||||
export const emptyTrashByWorkspace = mutation({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
args: { workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const membership = await ctx.db
|
||||
.query("workspace_members")
|
||||
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", args.userId))
|
||||
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
|
||||
.first();
|
||||
if (!membership) {
|
||||
throw new Error("无权操作该工作空间");
|
||||
@@ -286,7 +351,7 @@ export const emptyTrashByWorkspace = mutation({
|
||||
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
||||
.collect();
|
||||
|
||||
const toDelete = rows.filter((r) => r.user_id === args.userId && r.deleted_at != null);
|
||||
const toDelete = rows.filter((r) => r.user_id === userId && r.deleted_at != null);
|
||||
for (const r of toDelete) {
|
||||
await ctx.db.delete(r._id);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import { nowIso } from "./_utils/time";
|
||||
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
||||
|
||||
@@ -12,6 +13,14 @@ type WorkspaceSummary = {
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
async function requireUserId(ctx: any): Promise<string> {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (userId === null) {
|
||||
throw new Error("未登录");
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
async function findWorkspaceById(ctx: QueryCtx | MutationCtx, workspaceId: string) {
|
||||
return await ctx.db
|
||||
.query("workspaces")
|
||||
@@ -29,14 +38,15 @@ async function countMembers(ctx: QueryCtx | MutationCtx, workspaceId: string): P
|
||||
|
||||
export const ensureDefaultWorkspace = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
fallbackName: v.optional(v.string()),
|
||||
workspaceIdIfCreate: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const memberships = await ctx.db
|
||||
.query("workspace_members")
|
||||
.withIndex("by_user_id", (q) => q.eq("user_id", args.userId))
|
||||
.withIndex("by_user_id", (q) => q.eq("user_id", userId))
|
||||
.collect();
|
||||
|
||||
if (memberships.length === 0) {
|
||||
@@ -50,13 +60,13 @@ export const ensureDefaultWorkspace = mutation({
|
||||
name: workspaceName,
|
||||
type: "personal",
|
||||
icon_url: null,
|
||||
created_by: args.userId,
|
||||
created_by: userId,
|
||||
created_at: ts,
|
||||
});
|
||||
|
||||
await ctx.db.insert("workspace_members", {
|
||||
workspace_id: args.workspaceIdIfCreate,
|
||||
user_id: args.userId,
|
||||
user_id: userId,
|
||||
role: "owner",
|
||||
is_default: true,
|
||||
created_at: ts,
|
||||
@@ -103,12 +113,14 @@ export const ensureDefaultWorkspace = mutation({
|
||||
});
|
||||
|
||||
export const fetchWorkspaceSummaries = query({
|
||||
args: { userId: v.string() },
|
||||
args: {},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
// 说明:为了复用 ensureDefaultWorkspace 的返回结构,这里直接走同样的聚合逻辑。
|
||||
const memberships = await ctx.db
|
||||
.query("workspace_members")
|
||||
.withIndex("by_user_id", (q) => q.eq("user_id", args.userId))
|
||||
.withIndex("by_user_id", (q) => q.eq("user_id", userId))
|
||||
.collect();
|
||||
|
||||
const workspaceIds = Array.from(new Set(memberships.map((m) => m.workspace_id)));
|
||||
@@ -135,12 +147,14 @@ export const fetchWorkspaceSummaries = query({
|
||||
});
|
||||
|
||||
export const switchDefaultWorkspace = mutation({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
args: { workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
|
||||
const target = await ctx.db
|
||||
.query("workspace_members")
|
||||
.withIndex("by_workspace_user", (q) =>
|
||||
q.eq("workspace_id", args.workspaceId).eq("user_id", args.userId),
|
||||
q.eq("workspace_id", args.workspaceId).eq("user_id", userId),
|
||||
)
|
||||
.first();
|
||||
|
||||
@@ -150,7 +164,7 @@ export const switchDefaultWorkspace = mutation({
|
||||
|
||||
const memberships = await ctx.db
|
||||
.query("workspace_members")
|
||||
.withIndex("by_user_id", (q) => q.eq("user_id", args.userId))
|
||||
.withIndex("by_user_id", (q) => q.eq("user_id", userId))
|
||||
.collect();
|
||||
|
||||
// 说明:Convex 暂无批量 update,这里逐条 patch。
|
||||
|
||||
Reference in New Issue
Block a user