0.4.0 convex及界面修改
This commit is contained in:
@@ -178,6 +178,154 @@ async function purgeDocumentShareRelations(ctx: any, workspaceId: string, docume
|
||||
}
|
||||
}
|
||||
|
||||
async function purgeDocumentRelatedData(ctx: any, workspaceId: string, documentIds: string[]) {
|
||||
const uniqueDocIds = Array.from(new Set(documentIds.filter(Boolean)));
|
||||
if (uniqueDocIds.length === 0) return;
|
||||
|
||||
// 1) 思维导图
|
||||
for (const docId of uniqueDocIds) {
|
||||
const rows = await ctx.db
|
||||
.query("mindmaps")
|
||||
.withIndex("by_doc_mindmap", (q: any) => q.eq("document_id", docId))
|
||||
.collect();
|
||||
for (const r of rows) {
|
||||
await ctx.db.delete(r._id);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 附件(含底层 storage 文件)
|
||||
const assets: any[] = [];
|
||||
for (const docId of uniqueDocIds) {
|
||||
const rows = await ctx.db
|
||||
.query("media_assets")
|
||||
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
|
||||
.collect();
|
||||
assets.push(...rows);
|
||||
}
|
||||
|
||||
if (assets.length) {
|
||||
const assetIdsToDelete = new Set<string>(assets.map((a) => String(a.id)));
|
||||
const byStorage = new Map<string, string[]>();
|
||||
for (const a of assets) {
|
||||
const sid = (a.storage_id as any) ?? null;
|
||||
if (!sid) continue;
|
||||
const list = byStorage.get(String(sid)) ?? [];
|
||||
list.push(String(a.id));
|
||||
byStorage.set(String(sid), list);
|
||||
}
|
||||
|
||||
for (const [sid] of byStorage.entries()) {
|
||||
const refs = await ctx.db
|
||||
.query("media_assets")
|
||||
.withIndex("by_storage_id", (q: any) => q.eq("storage_id", sid as any))
|
||||
.collect();
|
||||
// 说明:只要仍有“未清理”的引用(包括仍在垃圾桶但可恢复的记录),就不要删底层文件。
|
||||
const otherAlive = refs.some((r: any) => !assetIdsToDelete.has(String(r.id)) && !r.purged_at);
|
||||
if (!otherAlive) {
|
||||
try {
|
||||
await ctx.storage.delete(sid as any);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const a of assets) {
|
||||
await ctx.db.delete(a._id);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 在线表格(含行数据)
|
||||
for (const docId of uniqueDocIds) {
|
||||
const tables = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
|
||||
.collect();
|
||||
for (const table of tables) {
|
||||
const rows = await ctx.db
|
||||
.query("document_table_rows")
|
||||
.withIndex("by_table", (q: any) => q.eq("table_id", table.id))
|
||||
.collect();
|
||||
for (const r of rows) {
|
||||
await ctx.db.delete(r._id);
|
||||
}
|
||||
await ctx.db.delete(table._id);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 评论
|
||||
for (const docId of uniqueDocIds) {
|
||||
const msgs = await ctx.db
|
||||
.query("comment_messages")
|
||||
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
|
||||
.collect();
|
||||
for (const m of msgs) {
|
||||
await ctx.db.delete(m._id);
|
||||
}
|
||||
|
||||
const threads = await ctx.db
|
||||
.query("comment_threads")
|
||||
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
|
||||
.collect();
|
||||
for (const t of threads) {
|
||||
await ctx.db.delete(t._id);
|
||||
}
|
||||
}
|
||||
|
||||
// 5) 页面引用/反链(source/target 任一命中即删除)
|
||||
const deletedRefIds = new Set<string>();
|
||||
for (const docId of uniqueDocIds) {
|
||||
const bySource = await ctx.db
|
||||
.query("page_references")
|
||||
.withIndex("by_workspace_source", (q: any) => q.eq("workspace_id", workspaceId).eq("source_page_id", docId))
|
||||
.collect();
|
||||
for (const r of bySource) {
|
||||
const id = String(r._id);
|
||||
if (deletedRefIds.has(id)) continue;
|
||||
deletedRefIds.add(id);
|
||||
await ctx.db.delete(r._id);
|
||||
}
|
||||
|
||||
const byTarget = await ctx.db
|
||||
.query("page_references")
|
||||
.withIndex("by_workspace_target", (q: any) => q.eq("workspace_id", workspaceId).eq("target_page_id", docId))
|
||||
.collect();
|
||||
for (const r of byTarget) {
|
||||
const id = String(r._id);
|
||||
if (deletedRefIds.has(id)) continue;
|
||||
deletedRefIds.add(id);
|
||||
await ctx.db.delete(r._id);
|
||||
}
|
||||
}
|
||||
|
||||
// 6) 收藏(避免残留用户数据)
|
||||
for (const docId of uniqueDocIds) {
|
||||
const stars = await ctx.db
|
||||
.query("document_stars")
|
||||
.withIndex("by_workspace_document_user", (q: any) => q.eq("workspace_id", workspaceId).eq("document_id", docId))
|
||||
.collect();
|
||||
for (const s of stars) {
|
||||
await ctx.db.delete(s._id);
|
||||
}
|
||||
}
|
||||
|
||||
// 7) 最近访问(避免“最近/历史”里出现幽灵页面)
|
||||
// 说明:user_recent_pages 当前缺少按 document_id 的索引,这里先全表扫描再过滤。
|
||||
// 若未来数据量变大,再考虑加 index("by_document", ["document_id"]) 或按 workspace/user 拆分索引。
|
||||
const recents = await ctx.db.query("user_recent_pages").collect();
|
||||
if (recents.length) {
|
||||
const docIdSet = new Set(uniqueDocIds);
|
||||
for (const r of recents) {
|
||||
if (!docIdSet.has(String((r as any).document_id ?? ""))) continue;
|
||||
await ctx.db.delete(r._id);
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:如果未来新增了其它 “document_id 外键表”,这里可以继续补充;
|
||||
// 当前先把最容易产生垃圾、且已出现历史堆积的表(media_assets/mindmaps/表格/评论/引用/收藏)清理掉。
|
||||
// 同时,避免误删“非 document_id 维度”的用户行为表(如 jobs 等),后续如确认需要可再补齐。
|
||||
}
|
||||
|
||||
export const getMeta = query({
|
||||
args: { id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
@@ -228,9 +376,17 @@ export const getMeta = query({
|
||||
show_structure: doc.show_structure ?? null,
|
||||
protect_editing: doc.protect_editing ?? null,
|
||||
show_word_count: doc.show_word_count ?? null,
|
||||
collapse_backlinks: (doc as any).collapse_backlinks ?? null,
|
||||
page_font: (doc as any).page_font ?? null,
|
||||
layout_density: (doc as any).layout_density ?? null,
|
||||
hide_child_pages: (doc as any).hide_child_pages ?? null,
|
||||
show_block_ref_count: (doc as any).show_block_ref_count ?? null,
|
||||
embed_default_block_id: (doc as any).embed_default_block_id ?? null,
|
||||
word_count: doc.word_count ?? null,
|
||||
character_count: doc.character_count ?? null,
|
||||
block_count: doc.block_count ?? null,
|
||||
todo_total_count: (doc as any).todo_total_count ?? null,
|
||||
todo_done_count: (doc as any).todo_done_count ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -674,9 +830,17 @@ export const create = mutation({
|
||||
show_structure: false,
|
||||
protect_editing: false,
|
||||
show_word_count: true,
|
||||
collapse_backlinks: false,
|
||||
page_font: "default",
|
||||
layout_density: "normal",
|
||||
hide_child_pages: false,
|
||||
show_block_ref_count: false,
|
||||
embed_default_block_id: null,
|
||||
word_count: 0,
|
||||
character_count: 0,
|
||||
block_count: 0,
|
||||
todo_total_count: 0,
|
||||
todo_done_count: 0,
|
||||
created_at: ts,
|
||||
updated_at: ts,
|
||||
deleted_at: null,
|
||||
@@ -751,6 +915,32 @@ export const updateTitle = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const setTemplate = mutation({
|
||||
args: { id: v.string(), isTemplate: v.boolean() },
|
||||
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.deleted_at != null) throw new Error("页面不存在");
|
||||
await requireWorkspaceMember(ctx, doc.workspace_id, userId);
|
||||
|
||||
if (doc.user_id !== userId) {
|
||||
if (doc.access_scope === "public") throw new Error("无权限");
|
||||
const perm =
|
||||
(await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
|
||||
if (perm !== "edit") throw new Error("无权限");
|
||||
}
|
||||
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { is_template: args.isTemplate, updated_at: ts });
|
||||
return { ok: true, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
export const move = mutation({
|
||||
args: {
|
||||
id: v.string(),
|
||||
@@ -941,6 +1131,7 @@ export const purge = mutation({
|
||||
const subtree = collectSubtree(owned, doc.id);
|
||||
|
||||
// 删除顺序对当前数据模型无强制要求,这里简单逐个删除即可。
|
||||
await purgeDocumentRelatedData(ctx, doc.workspace_id, subtree.map((d) => d.id));
|
||||
for (const item of subtree) {
|
||||
await purgeDocumentShareRelations(ctx, doc.workspace_id, item.id);
|
||||
await ctx.db.delete(item._id);
|
||||
@@ -971,6 +1162,7 @@ export const emptyTrashByWorkspace = mutation({
|
||||
.collect();
|
||||
|
||||
const toDelete = docs.filter((d) => d.user_id === userId && d.deleted_at != null);
|
||||
await purgeDocumentRelatedData(ctx, args.workspaceId, toDelete.map((d) => d.id));
|
||||
for (const d of toDelete) {
|
||||
await purgeDocumentShareRelations(ctx, args.workspaceId, d.id);
|
||||
await ctx.db.delete(d._id);
|
||||
@@ -991,6 +1183,12 @@ export const updateOptions = mutation({
|
||||
showStructure: v.optional(v.boolean()),
|
||||
protectEditing: v.optional(v.boolean()),
|
||||
showWordCount: v.optional(v.boolean()),
|
||||
collapseBacklinks: v.optional(v.boolean()),
|
||||
pageFont: v.optional(v.union(v.literal("default"), v.literal("song"), v.literal("kai"))),
|
||||
layoutDensity: v.optional(v.union(v.literal("compact"), v.literal("normal"), v.literal("spacious"))),
|
||||
hideChildPages: v.optional(v.boolean()),
|
||||
showBlockRefCount: v.optional(v.boolean()),
|
||||
embedDefaultBlockId: v.optional(v.union(v.string(), v.null())),
|
||||
}),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
@@ -1019,6 +1217,15 @@ export const updateOptions = mutation({
|
||||
if (typeof args.options.showStructure === "boolean") patch.show_structure = args.options.showStructure;
|
||||
if (typeof args.options.protectEditing === "boolean") patch.protect_editing = args.options.protectEditing;
|
||||
if (typeof args.options.showWordCount === "boolean") patch.show_word_count = args.options.showWordCount;
|
||||
if (typeof args.options.collapseBacklinks === "boolean") patch.collapse_backlinks = args.options.collapseBacklinks;
|
||||
if (typeof (args.options as any).pageFont === "string") patch.page_font = (args.options as any).pageFont;
|
||||
if (typeof (args.options as any).layoutDensity === "string") patch.layout_density = (args.options as any).layoutDensity;
|
||||
if (typeof (args.options as any).hideChildPages === "boolean") patch.hide_child_pages = (args.options as any).hideChildPages;
|
||||
if (typeof (args.options as any).showBlockRefCount === "boolean")
|
||||
patch.show_block_ref_count = (args.options as any).showBlockRefCount;
|
||||
if (typeof (args.options as any).embedDefaultBlockId === "string" || (args.options as any).embedDefaultBlockId === null) {
|
||||
patch.embed_default_block_id = (args.options as any).embedDefaultBlockId;
|
||||
}
|
||||
|
||||
if (Object.keys(patch).length === 0) {
|
||||
throw new Error("缺少可更新的选项");
|
||||
@@ -1036,6 +1243,8 @@ export const updateStats = mutation({
|
||||
wordCount: v.number(),
|
||||
characterCount: v.number(),
|
||||
blockCount: v.number(),
|
||||
todoTotal: v.optional(v.number()),
|
||||
todoDone: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireUserId(ctx);
|
||||
@@ -1058,6 +1267,8 @@ export const updateStats = mutation({
|
||||
word_count: args.wordCount,
|
||||
character_count: args.characterCount,
|
||||
block_count: args.blockCount,
|
||||
todo_total_count: typeof args.todoTotal === "number" ? args.todoTotal : (doc as any).todo_total_count ?? 0,
|
||||
todo_done_count: typeof args.todoDone === "number" ? args.todoDone : (doc as any).todo_done_count ?? 0,
|
||||
updated_at: ts,
|
||||
});
|
||||
return { ok: true, updated_at: ts };
|
||||
@@ -1113,9 +1324,17 @@ export const duplicate = mutation({
|
||||
show_structure: source.show_structure ?? false,
|
||||
protect_editing: source.protect_editing ?? false,
|
||||
show_word_count: source.show_word_count ?? true,
|
||||
collapse_backlinks: (source as any).collapse_backlinks ?? false,
|
||||
page_font: (source as any).page_font ?? "default",
|
||||
layout_density: (source as any).layout_density ?? "normal",
|
||||
hide_child_pages: (source as any).hide_child_pages ?? false,
|
||||
show_block_ref_count: (source as any).show_block_ref_count ?? false,
|
||||
embed_default_block_id: (source as any).embed_default_block_id ?? null,
|
||||
word_count: source.word_count ?? 0,
|
||||
character_count: source.character_count ?? 0,
|
||||
block_count: source.block_count ?? 0,
|
||||
todo_total_count: (source as any).todo_total_count ?? 0,
|
||||
todo_done_count: (source as any).todo_done_count ?? 0,
|
||||
|
||||
created_at: ts,
|
||||
updated_at: ts,
|
||||
|
||||
Reference in New Issue
Block a user