2026-02-01 08:47:40 +08:00
|
|
|
import { internalMutation, internalQuery, mutation, query } from "./_generated/server";
|
2026-01-17 10:12:53 +08:00
|
|
|
import { v } from "convex/values";
|
2026-04-14 13:22:29 +08:00
|
|
|
import { requireUserId } from "./_utils/auth";
|
2026-01-17 10:12:53 +08:00
|
|
|
import { nowIso } from "./_utils/time";
|
2026-01-18 05:13:53 +08:00
|
|
|
import { enqueueIngestMindmapJob } from "./_utils/ingestJobs";
|
2026-02-01 08:47:40 +08:00
|
|
|
import { internal } from "./_generated/api";
|
2026-04-14 13:22:29 +08:00
|
|
|
import { requireCanonicalOwnedDocument } from "./_utils/documentRecord";
|
2026-01-17 10:12:53 +08:00
|
|
|
|
|
|
|
|
const defaultMindmapData = {
|
|
|
|
|
data: { text: "中心主题" },
|
|
|
|
|
children: [],
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-01 08:47:40 +08:00
|
|
|
function resolveGraceSeconds(): number {
|
|
|
|
|
const raw = process.env.DELETE_GRACE_SECONDS ?? process.env.NEXT_PUBLIC_DELETE_GRACE_SECONDS ?? "600";
|
|
|
|
|
const parsed = Number(raw);
|
|
|
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
|
|
|
return 600;
|
|
|
|
|
}
|
|
|
|
|
return Math.floor(parsed);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
function normalizeMindmapId(docId: string, mindmapId: string): string {
|
|
|
|
|
const raw = String(mindmapId ?? "").trim();
|
|
|
|
|
if (raw) return raw;
|
|
|
|
|
// 兜底:不允许空 mindmapId(否则无法索引)
|
|
|
|
|
return `legacy-${docId}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requireOwnedDocument(ctx: any, userId: string, docId: string) {
|
2026-04-14 13:22:29 +08:00
|
|
|
return await requireCanonicalOwnedDocument<any>(ctx, docId, userId);
|
2026-01-17 10:12:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const get = query({
|
2026-01-18 19:01:31 +08:00
|
|
|
args: { docId: v.string(), mindmapId: v.string() },
|
2026-01-17 10:12:53 +08:00
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
|
|
|
|
const doc = await requireOwnedDocument(ctx, userId, args.docId);
|
2026-01-17 10:12:53 +08:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-18 19:01:31 +08:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
export const put = mutation({
|
|
|
|
|
args: {
|
|
|
|
|
docId: v.string(),
|
|
|
|
|
mindmapId: v.string(),
|
|
|
|
|
data: v.any(),
|
|
|
|
|
createOnly: v.optional(v.boolean()),
|
|
|
|
|
},
|
|
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
|
|
|
|
const doc = await requireOwnedDocument(ctx, userId, args.docId);
|
2026-01-17 10:12:53 +08:00
|
|
|
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
|
|
|
|
|
|
|
|
|
const existing = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_doc_mindmap", (q) => q.eq("document_id", args.docId).eq("mindmap_id", mindmapId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
const ts = nowIso();
|
|
|
|
|
const payload = args.data ?? defaultMindmapData;
|
|
|
|
|
|
|
|
|
|
if (existing && existing.deleted_at == null && args.createOnly) {
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
created: false,
|
|
|
|
|
skipped: true,
|
|
|
|
|
workspace_id: doc.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
updated_at: existing.updated_at ?? null,
|
|
|
|
|
};
|
2026-01-17 10:12:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
await ctx.db.patch(existing._id, {
|
|
|
|
|
data: payload,
|
|
|
|
|
updated_at: ts,
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
deleted_by: null,
|
|
|
|
|
});
|
2026-01-18 19:01:31 +08:00
|
|
|
await enqueueIngestMindmapJob(ctx, { userId, docId: args.docId, mindmapId, debounceMs: 1500 });
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
created: false,
|
|
|
|
|
skipped: false,
|
|
|
|
|
workspace_id: doc.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
updated_at: ts,
|
|
|
|
|
};
|
2026-01-17 10:12:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ctx.db.insert("mindmaps", {
|
|
|
|
|
id: `${args.docId}:${mindmapId}`,
|
2026-01-18 19:01:31 +08:00
|
|
|
user_id: userId,
|
2026-01-17 10:12:53 +08:00
|
|
|
workspace_id: doc.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
data: payload,
|
|
|
|
|
created_at: ts,
|
|
|
|
|
updated_at: ts,
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
deleted_by: null,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-18 19:01:31 +08:00
|
|
|
await enqueueIngestMindmapJob(ctx, { userId, docId: args.docId, mindmapId, debounceMs: 1500 });
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
created: true,
|
|
|
|
|
skipped: false,
|
|
|
|
|
workspace_id: doc.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
updated_at: ts,
|
|
|
|
|
};
|
2026-01-17 10:12:53 +08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const softDelete = mutation({
|
2026-01-18 19:01:31 +08:00
|
|
|
args: { docId: v.string(), mindmapId: v.string() },
|
2026-01-17 10:12:53 +08:00
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
|
|
|
|
await requireOwnedDocument(ctx, userId, args.docId);
|
2026-01-17 10:12:53 +08:00
|
|
|
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
|
|
|
|
|
|
|
|
|
const existing = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_doc_mindmap", (q) => q.eq("document_id", args.docId).eq("mindmap_id", mindmapId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
|
// 兼容:不存在也视为成功
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
moved: 0,
|
|
|
|
|
workspace_id: null,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
};
|
2026-01-17 10:12:53 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 08:47:40 +08:00
|
|
|
if (existing.deleted_at != null) {
|
|
|
|
|
// 已在垃圾桶:保持幂等,避免重复删除导致 updated_at 抖动/重复调度
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
moved: 0,
|
|
|
|
|
workspace_id: existing.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
deleted_at: existing.deleted_at,
|
|
|
|
|
};
|
2026-02-01 08:47:40 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
const ts = nowIso();
|
2026-01-18 19:01:31 +08:00
|
|
|
await ctx.db.patch(existing._id, { deleted_at: ts, deleted_by: userId, updated_at: ts });
|
2026-02-01 08:47:40 +08:00
|
|
|
|
|
|
|
|
// 10 分钟宽限(可恢复);到期自动清理(彻底删除)。
|
|
|
|
|
// 说明:这里用 scheduler.runAfter 做“按条目调度”,无需全局 cron。
|
|
|
|
|
const graceMs = (resolveGraceSeconds() + 5) * 1000;
|
|
|
|
|
await ctx.scheduler.runAfter(graceMs, internal.mindmaps.purgeIfExpired, {
|
|
|
|
|
docId: args.docId,
|
|
|
|
|
mindmapId,
|
|
|
|
|
deletedAt: ts,
|
|
|
|
|
});
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
moved: 1,
|
|
|
|
|
workspace_id: existing.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
deleted_at: ts,
|
|
|
|
|
};
|
2026-02-01 08:47:40 +08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const purgeIfExpired = internalMutation({
|
|
|
|
|
args: { docId: v.string(), mindmapId: v.string(), deletedAt: v.string() },
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
|
|
|
|
|
|
|
|
|
const existing = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_doc_mindmap", (q) => q.eq("document_id", args.docId).eq("mindmap_id", mindmapId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
|
return { ok: true, deleted: false, reason: "not_found" as const };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existing.deleted_at == null) {
|
|
|
|
|
return { ok: true, deleted: false, reason: "restored" as const };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existing.deleted_at !== args.deletedAt) {
|
|
|
|
|
// 删除时间不一致:说明已被恢复/重新删除过,旧调度作废
|
|
|
|
|
return { ok: true, deleted: false, reason: "changed" as const };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const graceSeconds = resolveGraceSeconds();
|
|
|
|
|
const deletedTs = Date.parse(existing.deleted_at);
|
|
|
|
|
if (Number.isFinite(deletedTs)) {
|
|
|
|
|
const elapsedMs = Date.now() - deletedTs;
|
|
|
|
|
const graceMs = graceSeconds * 1000;
|
|
|
|
|
if (elapsedMs < graceMs) {
|
|
|
|
|
// 兜底:调度延迟/时钟偏差时,重新排一次剩余时间
|
|
|
|
|
const remainingMs = Math.max(1000, graceMs - elapsedMs + 5000);
|
|
|
|
|
await ctx.scheduler.runAfter(remainingMs, internal.mindmaps.purgeIfExpired, {
|
|
|
|
|
docId: args.docId,
|
|
|
|
|
mindmapId,
|
|
|
|
|
deletedAt: args.deletedAt,
|
|
|
|
|
});
|
|
|
|
|
return { ok: true, deleted: false, rescheduled: true, remainingMs };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ctx.db.delete(existing._id);
|
|
|
|
|
return { ok: true, deleted: true };
|
2026-01-17 10:12:53 +08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const restore = mutation({
|
2026-01-18 19:01:31 +08:00
|
|
|
args: { docId: v.string(), mindmapId: v.string() },
|
2026-01-17 10:12:53 +08:00
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
|
|
|
|
await requireOwnedDocument(ctx, userId, args.docId);
|
2026-01-17 10:12:53 +08:00
|
|
|
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
|
|
|
|
|
|
|
|
|
const existing = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_doc_mindmap", (q) => q.eq("document_id", args.docId).eq("mindmap_id", mindmapId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
|
throw new Error("未找到可操作的记录");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ts = nowIso();
|
|
|
|
|
await ctx.db.patch(existing._id, { deleted_at: null, deleted_by: null, updated_at: ts });
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
workspace_id: existing.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
updated_at: ts,
|
|
|
|
|
};
|
2026-01-17 10:12:53 +08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const purge = mutation({
|
2026-01-18 19:01:31 +08:00
|
|
|
args: { docId: v.string(), mindmapId: v.string() },
|
2026-01-17 10:12:53 +08:00
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
|
|
|
|
await requireOwnedDocument(ctx, userId, args.docId);
|
2026-01-17 10:12:53 +08:00
|
|
|
const mindmapId = normalizeMindmapId(args.docId, args.mindmapId);
|
|
|
|
|
|
|
|
|
|
const existing = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_doc_mindmap", (q) => q.eq("document_id", args.docId).eq("mindmap_id", mindmapId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
|
throw new Error("未找到可操作的记录");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ctx.db.delete(existing._id);
|
2026-04-15 03:06:29 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
workspace_id: existing.workspace_id,
|
|
|
|
|
document_id: args.docId,
|
|
|
|
|
mindmap_id: mindmapId,
|
|
|
|
|
};
|
2026-01-17 10:12:53 +08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const listByWorkspace = query({
|
2026-01-18 19:01:31 +08:00
|
|
|
args: { workspaceId: v.string(), includeDeleted: v.optional(v.boolean()) },
|
2026-01-17 10:12:53 +08:00
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
// 说明:阶段 6 先按 membership 存在即可,避免引入复杂权限模型。
|
|
|
|
|
const membership = await ctx.db
|
|
|
|
|
.query("workspace_members")
|
2026-01-18 19:01:31 +08:00
|
|
|
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
|
2026-01-17 10:12:53 +08:00
|
|
|
.first();
|
|
|
|
|
if (!membership) {
|
|
|
|
|
throw new Error("无权操作该工作空间");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rows = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
const includeDeleted = Boolean(args.includeDeleted);
|
|
|
|
|
|
|
|
|
|
return rows
|
2026-01-18 19:01:31 +08:00
|
|
|
.filter((r) => r.user_id === userId)
|
2026-01-17 10:12:53 +08:00
|
|
|
.filter((r) => (includeDeleted ? true : r.deleted_at == null))
|
|
|
|
|
.sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""))
|
|
|
|
|
.map((r) => ({
|
|
|
|
|
id: r.id,
|
|
|
|
|
workspace_id: r.workspace_id,
|
|
|
|
|
document_id: r.document_id,
|
|
|
|
|
mindmap_id: r.mindmap_id,
|
|
|
|
|
data: r.data ?? defaultMindmapData,
|
|
|
|
|
created_at: r.created_at,
|
|
|
|
|
updated_at: r.updated_at,
|
|
|
|
|
deleted_at: r.deleted_at,
|
|
|
|
|
deleted_by: r.deleted_by,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const copyByDocument = mutation({
|
2026-01-18 19:01:31 +08:00
|
|
|
args: { sourceDocId: v.string(), targetDocId: v.string() },
|
2026-01-17 10:12:53 +08:00
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
|
|
|
|
const sourceDoc = await requireOwnedDocument(ctx, userId, args.sourceDocId);
|
|
|
|
|
const targetDoc = await requireOwnedDocument(ctx, userId, args.targetDocId);
|
2026-01-17 10:12:53 +08:00
|
|
|
|
|
|
|
|
// 说明:同一工作空间复制最常见;若跨工作空间复制,这里仍允许,但 mindmaps 会落到目标页面的 workspace_id。
|
|
|
|
|
const sourceRows = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_workspace", (q) => q.eq("workspace_id", sourceDoc.workspace_id))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
const sourceMindmaps = sourceRows
|
2026-01-18 19:01:31 +08:00
|
|
|
.filter((r) => r.user_id === userId)
|
2026-01-17 10:12:53 +08:00
|
|
|
.filter((r) => r.document_id === args.sourceDocId)
|
|
|
|
|
.filter((r) => r.deleted_at == null);
|
|
|
|
|
|
|
|
|
|
let copied = 0;
|
|
|
|
|
const ts = nowIso();
|
|
|
|
|
|
|
|
|
|
for (const r of sourceMindmaps) {
|
|
|
|
|
const existing = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_doc_mindmap", (q) => q.eq("document_id", args.targetDocId).eq("mindmap_id", r.mindmap_id))
|
|
|
|
|
.first();
|
|
|
|
|
if (existing) continue;
|
|
|
|
|
|
|
|
|
|
await ctx.db.insert("mindmaps", {
|
|
|
|
|
id: `${args.targetDocId}:${r.mindmap_id}`,
|
2026-01-18 19:01:31 +08:00
|
|
|
user_id: userId,
|
2026-01-17 10:12:53 +08:00
|
|
|
workspace_id: targetDoc.workspace_id,
|
|
|
|
|
document_id: args.targetDocId,
|
|
|
|
|
mindmap_id: r.mindmap_id,
|
|
|
|
|
data: r.data ?? defaultMindmapData,
|
|
|
|
|
created_at: ts,
|
|
|
|
|
updated_at: ts,
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
deleted_by: null,
|
|
|
|
|
});
|
|
|
|
|
copied += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ok: true, copied };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const emptyTrashByWorkspace = mutation({
|
2026-01-18 19:01:31 +08:00
|
|
|
args: { workspaceId: v.string() },
|
2026-01-17 10:12:53 +08:00
|
|
|
handler: async (ctx, args) => {
|
2026-01-18 19:01:31 +08:00
|
|
|
const userId = await requireUserId(ctx);
|
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
const membership = await ctx.db
|
|
|
|
|
.query("workspace_members")
|
2026-01-18 19:01:31 +08:00
|
|
|
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
|
2026-01-17 10:12:53 +08:00
|
|
|
.first();
|
|
|
|
|
if (!membership) {
|
|
|
|
|
throw new Error("无权操作该工作空间");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rows = await ctx.db
|
|
|
|
|
.query("mindmaps")
|
|
|
|
|
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
|
|
|
|
.collect();
|
|
|
|
|
|
2026-01-18 19:01:31 +08:00
|
|
|
const toDelete = rows.filter((r) => r.user_id === userId && r.deleted_at != null);
|
2026-01-17 10:12:53 +08:00
|
|
|
for (const r of toDelete) {
|
|
|
|
|
await ctx.db.delete(r._id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ok: true, deletedCount: toDelete.length };
|
|
|
|
|
},
|
|
|
|
|
});
|