Files
mnote/wolai-frontend/convex/tables.ts
T

290 lines
7.9 KiB
TypeScript
Raw Normal View History

2026-01-18 05:13:53 +08:00
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { nowIso } from "./_utils/time";
import { generateId } from "./_utils/id";
// Query: 获取单个表格
export const get = query({
args: { userId: v.string(), tableId: v.string() },
handler: async (ctx, args) => {
const table = await ctx.db
.query("document_tables")
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
.first();
if (!table) return null;
if (table.workspace_id) {
// 验证用户是否在工作区中(简化版:假设 userId 有效)
// TODO: 添加 workspace_members 验证
}
return {
id: table.id,
workspace_id: table.workspace_id,
document_id: table.document_id,
grid_key: table.grid_key,
title: table.title,
schema: table.schema,
view_preferences: table.view_preferences,
snapshot: table.snapshot ?? null,
is_archived: table.is_archived,
last_synced_at: table.last_synced_at ?? null,
created_by: table.created_by,
updated_by: table.updated_by ?? null,
created_at: table.created_at,
updated_at: table.updated_at,
};
},
});
// Query: 通过 gridKey 获取表格(用于 Luckysheet get-workerbook
export const getByGridKey = query({
args: { gridKey: v.string() },
handler: async (ctx, args) => {
const table = await ctx.db
.query("document_tables")
.withIndex("by_grid_key", (q) => q.eq("grid_key", args.gridKey))
.first();
if (!table) return null;
// 返回 Luckysheet 需要的格式
return {
title: table.title,
gridKey: table.grid_key,
lang: "zh",
};
},
});
// Query: 通过 gridKey 获取完整表格数据(用于 Luckysheet load
export const getByGridKeyFull = query({
args: { gridKey: v.string() },
handler: async (ctx, args) => {
const table = await ctx.db
.query("document_tables")
.withIndex("by_grid_key", (q) => q.eq("grid_key", args.gridKey))
.first();
if (!table) return null;
// 返回完整的表格数据,包括 schema 和 snapshot
return {
id: table.id,
grid_key: table.grid_key,
title: table.title,
schema: table.schema,
snapshot: table.snapshot ?? null,
};
},
});
// Query: 列出文档的所有表格
export const listByDocument = query({
args: { userId: v.string(), documentId: v.string() },
handler: async (ctx, args) => {
const tables = await ctx.db
.query("document_tables")
.withIndex("by_document", (q) => q.eq("document_id", args.documentId))
.collect();
return tables
.filter((t) => !t.is_archived)
.map((t) => ({
id: t.id,
document_id: t.document_id,
grid_key: t.grid_key,
title: t.title,
schema: t.schema,
view_preferences: t.view_preferences,
snapshot: t.snapshot ?? null,
is_archived: t.is_archived,
last_synced_at: t.last_synced_at ?? null,
created_at: t.created_at,
updated_at: t.updated_at,
}));
},
});
// Mutation: 创建表格
export const create = mutation({
args: {
userId: v.string(),
workspaceId: v.string(),
documentId: v.string(),
title: v.optional(v.string()),
schema: v.optional(v.any()),
snapshot: v.optional(v.any()),
},
handler: async (ctx, args) => {
const now = nowIso();
const tableId = generateId();
const gridKey = generateId(); // 用于 WebSocket 协同标识
const table = {
id: tableId,
workspace_id: args.workspaceId,
document_id: args.documentId,
grid_key: gridKey,
title: args.title ?? "未命名表格",
schema: args.schema ?? {},
view_preferences: {},
snapshot: args.snapshot ?? null,
is_archived: false,
last_synced_at: now,
created_by: args.userId,
updated_by: args.userId,
created_at: now,
updated_at: now,
};
await ctx.db.insert("document_tables", table);
return {
id: table.id,
grid_key: table.grid_key,
title: table.title,
schema: table.schema,
snapshot: table.snapshot,
};
},
});
// Mutation: 更新表格
export const update = mutation({
args: {
userId: v.string(),
tableId: v.string(),
title: v.optional(v.string()),
schema: v.optional(v.any()),
view_preferences: v.optional(v.any()),
snapshot: v.optional(v.any()),
rows: v.optional(v.any()),
},
handler: async (ctx, args) => {
const table = await ctx.db
.query("document_tables")
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
.first();
if (!table) {
throw new Error("Table not found");
}
const now = nowIso();
const updates: any = {
updated_at: now,
updated_by: args.userId,
last_synced_at: now,
};
if (args.title !== undefined) updates.title = args.title;
if (args.schema !== undefined) updates.schema = args.schema;
if (args.view_preferences !== undefined) updates.view_preferences = args.view_preferences;
if (args.snapshot !== undefined) updates.snapshot = args.snapshot;
await ctx.db.patch(table._id, updates);
// 如果提供了 rows,更新行数据
if (args.rows && Array.isArray(args.rows)) {
// 先删除旧行
const existingRows = await ctx.db
.query("document_table_rows")
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
.collect();
for (const row of existingRows) {
await ctx.db.delete(row._id);
}
// 插入新行
for (const [index, rowData] of args.rows.entries()) {
await ctx.db.insert("document_table_rows", {
id: generateId(),
workspace_id: table.workspace_id,
document_id: table.document_id,
table_id: args.tableId,
row_index: index,
row_data: rowData,
row_hash: JSON.stringify(rowData),
is_deleted: false,
updated_by: args.userId,
created_at: now,
updated_at: now,
});
}
}
return {
id: table.id,
title: updates.title ?? table.title,
schema: updates.schema ?? table.schema,
view_preferences: updates.view_preferences ?? table.view_preferences,
snapshot: updates.snapshot ?? table.snapshot,
updated_at: now,
};
},
});
// Mutation: 删除表格(软删除)
export const remove = mutation({
args: { userId: v.string(), tableId: v.string() },
handler: async (ctx, args) => {
const table = await ctx.db
.query("document_tables")
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
.first();
if (!table) {
throw new Error("Table not found");
}
await ctx.db.patch(table._id, {
is_archived: true,
updated_at: nowIso(),
updated_by: args.userId,
});
return { success: true };
},
});
// Mutation: 永久删除表格
export const purge = mutation({
args: { userId: v.string(), tableId: v.string() },
handler: async (ctx, args) => {
const table = await ctx.db
.query("document_tables")
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
.first();
if (!table) {
throw new Error("Table not found");
}
// 删除关联的行
const rows = await ctx.db
.query("document_table_rows")
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
.collect();
for (const row of rows) {
await ctx.db.delete(row._id);
}
// 删除表格
await ctx.db.delete(table._id);
return { success: true };
},
});
// Query: 获取表格行数据
export const getRows = query({
args: { tableId: v.string() },
handler: async (ctx, args) => {
const rows = await ctx.db
.query("document_table_rows")
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
.collect();
return rows
.filter((r) => !r.is_deleted)
.sort((a, b) => a.row_index - b.row_index)
.map((r) => r.row_data);
},
});