0.2.1 onlyoffice修复
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { nowIso } from "./_utils/time";
|
||||
|
||||
const accessScope = v.union(v.literal("private"), v.literal("shared"), v.literal("public"));
|
||||
|
||||
export const getMeta = query({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
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 !== args.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 getContent = query({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
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 !== args.userId) return null;
|
||||
return { content: doc.content ?? null };
|
||||
},
|
||||
});
|
||||
|
||||
export const listByWorkspace = query({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const docs = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
|
||||
.collect();
|
||||
|
||||
// 说明:阶段 4 先不做垃圾桶(deleted_at != null),因此这里直接过滤。
|
||||
return docs
|
||||
.filter((d) => d.user_id === args.userId)
|
||||
.filter((d) => d.deleted_at == null)
|
||||
.map((d) => ({
|
||||
access_scope: d.access_scope,
|
||||
id: d.id,
|
||||
workspace_id: d.workspace_id,
|
||||
title: d.title ?? "无标题",
|
||||
parent_id: d.parent_id ?? null,
|
||||
sort_order: d.sort_order ?? null,
|
||||
is_starred: d.is_starred ?? null,
|
||||
is_template: d.is_template ?? false,
|
||||
created_at: d.created_at,
|
||||
updated_at: d.updated_at ?? null,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export const listTrashedByWorkspace = query({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
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.deleted_at != null)
|
||||
.sort((a, b) => (b.deleted_at ?? "").localeCompare(a.deleted_at ?? ""))
|
||||
.slice(0, 100)
|
||||
.map((d) => ({
|
||||
id: d.id,
|
||||
title: d.title ?? null,
|
||||
parent_id: d.parent_id ?? null,
|
||||
deleted_at: d.deleted_at!,
|
||||
access_scope: d.access_scope,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
id: v.string(),
|
||||
workspaceId: v.string(),
|
||||
parentId: v.union(v.string(), v.null()),
|
||||
title: v.optional(v.union(v.string(), v.null())),
|
||||
accessScope,
|
||||
content: v.optional(v.any()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const siblings = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace_parent", (q) => q.eq("workspace_id", args.workspaceId).eq("parent_id", args.parentId))
|
||||
.collect();
|
||||
|
||||
const sortOrder = siblings.filter((d) => d.deleted_at == null).length;
|
||||
const ts = nowIso();
|
||||
|
||||
const title = (args.title ?? "无标题") || "无标题";
|
||||
const content = typeof args.content === "undefined" ? [] : args.content;
|
||||
|
||||
await ctx.db.insert("documents", {
|
||||
id: args.id,
|
||||
user_id: args.userId,
|
||||
workspace_id: args.workspaceId,
|
||||
parent_id: args.parentId,
|
||||
title,
|
||||
content,
|
||||
access_scope: args.accessScope,
|
||||
sort_order: sortOrder,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
|
||||
wide_layout: false,
|
||||
use_small_text: false,
|
||||
show_heading_numbers: true,
|
||||
show_toc: false,
|
||||
show_structure: false,
|
||||
protect_editing: false,
|
||||
show_word_count: true,
|
||||
word_count: 0,
|
||||
character_count: 0,
|
||||
block_count: 0,
|
||||
created_at: ts,
|
||||
updated_at: ts,
|
||||
deleted_at: null,
|
||||
deleted_by: null,
|
||||
});
|
||||
|
||||
return {
|
||||
id: args.id,
|
||||
title,
|
||||
parent_id: args.parentId,
|
||||
sort_order: sortOrder,
|
||||
is_starred: false,
|
||||
created_at: ts,
|
||||
updated_at: ts,
|
||||
workspace_id: args.workspaceId,
|
||||
access_scope: args.accessScope,
|
||||
is_template: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const updateContent = mutation({
|
||||
args: { userId: v.string(), id: v.string(), content: v.any() },
|
||||
handler: async (ctx, args) => {
|
||||
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("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { content: args.content, updated_at: ts });
|
||||
return { ok: true, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
export const updateTitle = mutation({
|
||||
args: { userId: v.string(), id: v.string(), title: v.union(v.string(), v.null()) },
|
||||
handler: async (ctx, args) => {
|
||||
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("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { title: args.title, updated_at: ts });
|
||||
return { ok: true, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
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 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("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, {
|
||||
parent_id: args.parentId,
|
||||
sort_order: args.sortOrder,
|
||||
updated_at: ts,
|
||||
});
|
||||
return { ok: true, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
export const softDelete = mutation({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
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("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { deleted_at: ts, deleted_by: args.userId, updated_at: ts });
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const restore = mutation({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
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("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, {
|
||||
deleted_at: null,
|
||||
deleted_by: null,
|
||||
parent_id: null,
|
||||
access_scope: "private",
|
||||
updated_at: ts,
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const purge = mutation({
|
||||
args: { userId: v.string(), id: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
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("无权限");
|
||||
await ctx.db.delete(doc._id);
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const emptyTrashByWorkspace = mutation({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
// 说明:阶段 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))
|
||||
.first();
|
||||
|
||||
if (!membership) {
|
||||
throw new Error("无权操作该工作空间");
|
||||
}
|
||||
|
||||
const docs = await ctx.db
|
||||
.query("documents")
|
||||
.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);
|
||||
for (const d of toDelete) {
|
||||
await ctx.db.delete(d._id);
|
||||
}
|
||||
|
||||
return { ok: true, deletedCount: toDelete.length };
|
||||
},
|
||||
});
|
||||
|
||||
export const updateOptions = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
id: v.string(),
|
||||
options: v.object({
|
||||
wideLayout: v.optional(v.boolean()),
|
||||
smallText: v.optional(v.boolean()),
|
||||
showHeadingNumbers: v.optional(v.boolean()),
|
||||
showToc: v.optional(v.boolean()),
|
||||
showStructure: v.optional(v.boolean()),
|
||||
protectEditing: v.optional(v.boolean()),
|
||||
showWordCount: v.optional(v.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
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("无权限");
|
||||
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (typeof args.options.wideLayout === "boolean") patch.wide_layout = args.options.wideLayout;
|
||||
if (typeof args.options.smallText === "boolean") patch.use_small_text = args.options.smallText;
|
||||
if (typeof args.options.showHeadingNumbers === "boolean")
|
||||
patch.show_heading_numbers = args.options.showHeadingNumbers;
|
||||
if (typeof args.options.showToc === "boolean") patch.show_toc = args.options.showToc;
|
||||
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 (Object.keys(patch).length === 0) {
|
||||
throw new Error("缺少可更新的选项");
|
||||
}
|
||||
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, { ...patch, updated_at: ts });
|
||||
return { ok: true, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
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 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("无权限");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(doc._id, {
|
||||
word_count: args.wordCount,
|
||||
character_count: args.characterCount,
|
||||
block_count: args.blockCount,
|
||||
updated_at: ts,
|
||||
});
|
||||
return { ok: true, updated_at: ts };
|
||||
},
|
||||
});
|
||||
|
||||
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 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("页面不存在或无权限访问");
|
||||
|
||||
const siblings = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace_parent", (q) =>
|
||||
q.eq("workspace_id", source.workspace_id).eq("parent_id", source.parent_id),
|
||||
)
|
||||
.collect();
|
||||
|
||||
const sortOrder = siblings.filter((d) => d.deleted_at == null).length;
|
||||
const ts = nowIso();
|
||||
const baseTitle = (source.title ?? "无标题").trim() ? (source.title ?? "无标题").trim() : "无标题";
|
||||
const title = (args.title ?? `${baseTitle} 副本`) || `${baseTitle} 副本`;
|
||||
|
||||
await ctx.db.insert("documents", {
|
||||
id: args.newId,
|
||||
user_id: args.userId,
|
||||
workspace_id: source.workspace_id,
|
||||
parent_id: source.parent_id,
|
||||
title,
|
||||
content: source.content ?? [],
|
||||
access_scope: source.access_scope,
|
||||
sort_order: sortOrder,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
|
||||
wide_layout: source.wide_layout ?? false,
|
||||
use_small_text: source.use_small_text ?? false,
|
||||
show_heading_numbers: source.show_heading_numbers ?? true,
|
||||
show_toc: source.show_toc ?? false,
|
||||
show_structure: source.show_structure ?? false,
|
||||
protect_editing: source.protect_editing ?? false,
|
||||
show_word_count: source.show_word_count ?? true,
|
||||
word_count: source.word_count ?? 0,
|
||||
character_count: source.character_count ?? 0,
|
||||
block_count: source.block_count ?? 0,
|
||||
|
||||
created_at: ts,
|
||||
updated_at: ts,
|
||||
deleted_at: null,
|
||||
deleted_by: null,
|
||||
});
|
||||
|
||||
return {
|
||||
id: args.newId,
|
||||
title,
|
||||
parent_id: source.parent_id ?? null,
|
||||
sort_order: sortOrder,
|
||||
workspace_id: source.workspace_id,
|
||||
access_scope: source.access_scope,
|
||||
created_at: ts,
|
||||
updated_at: ts,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const listAllForCopy = query({
|
||||
args: { userId: v.string(), workspaceId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
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.deleted_at == null)
|
||||
.map((d) => ({
|
||||
id: d.id,
|
||||
title: d.title ?? null,
|
||||
parent_id: d.parent_id ?? null,
|
||||
workspace_id: d.workspace_id,
|
||||
access_scope: d.access_scope,
|
||||
sort_order: d.sort_order ?? null,
|
||||
created_at: d.created_at ?? null,
|
||||
content: d.content ?? null,
|
||||
}));
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user