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

191 lines
7.1 KiB
TypeScript
Raw Normal View History

2026-01-17 10:12:53 +08:00
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
// 说明:
// - 这里先按“兼容现有 Next API 返回结构”的思路设计字段:id/workspace_id/user_id 等命名保持与 Supabase 一致。
// - Convex 自带的 _id 仍然存在,但我们暂时不把它暴露给上层业务,便于逐步迁移与回退。
export default defineSchema({
workspaces: defineTable({
id: v.string(),
name: v.string(),
type: v.union(v.literal("personal"), v.literal("team")),
icon_url: v.union(v.string(), v.null()),
created_by: v.string(),
created_at: v.string(),
})
.index("by_workspace_id", ["id"])
.index("by_created_by", ["created_by"]),
workspace_members: defineTable({
workspace_id: v.string(),
user_id: v.string(),
role: v.string(),
is_default: v.boolean(),
created_at: v.string(),
})
.index("by_user_id", ["user_id"])
.index("by_workspace_user", ["workspace_id", "user_id"])
.index("by_workspace_id", ["workspace_id"]),
documents: defineTable({
id: v.string(),
user_id: v.string(),
workspace_id: v.string(),
title: v.union(v.string(), v.null()),
parent_id: v.union(v.string(), v.null()),
sort_order: v.union(v.number(), v.null()),
is_starred: v.union(v.boolean(), v.null()),
is_template: v.boolean(),
access_scope: v.union(v.literal("private"), v.literal("shared"), v.literal("public")),
// 页面选项(对应 Supabase documents 上的 UI 配置列)
wide_layout: v.union(v.boolean(), v.null()),
use_small_text: v.union(v.boolean(), v.null()),
show_heading_numbers: v.union(v.boolean(), v.null()),
show_toc: v.union(v.boolean(), v.null()),
show_structure: v.union(v.boolean(), v.null()),
protect_editing: v.union(v.boolean(), v.null()),
show_word_count: v.union(v.boolean(), v.null()),
// 统计信息(由客户端编辑器计算后回写)
word_count: v.union(v.number(), v.null()),
character_count: v.union(v.number(), v.null()),
block_count: v.union(v.number(), v.null()),
// 说明:当前文档内容结构还在演进,先用 any 承接(与 Supabase Json 一致的宽松形态)。
content: v.any(),
// 说明:后续可用于搜索/索引(目前先留空,不强制写入)。
raw_text: v.optional(v.union(v.string(), v.null())),
// 时间戳统一使用 ISO 字符串,便于直接复用前端现有排序逻辑。
created_at: v.string(),
updated_at: v.union(v.string(), v.null()),
// 软删除(阶段 4 先不实现垃圾桶逻辑,但字段先留好,便于后续迁移)。
deleted_at: v.union(v.string(), v.null()),
deleted_by: v.union(v.string(), v.null()),
// 兼容旧逻辑:Supabase documents.mindmap_data。
mindmap_data: v.optional(v.any()),
})
.index("by_document_id", ["id"])
.index("by_user", ["user_id"])
.index("by_workspace", ["workspace_id"])
.index("by_workspace_parent", ["workspace_id", "parent_id"]),
// 最近访问(替代 Supabase user_recent_pages
user_recent_pages: defineTable({
user_id: v.string(),
workspace_id: v.string(),
document_id: v.string(),
last_accessed_at: v.string(),
})
.index("by_user_workspace", ["user_id", "workspace_id"])
.index("by_user_document", ["user_id", "document_id"]),
// 阶段 5:异步任务/队列表(最小骨架,后续可扩展为通用作业系统)
jobs: defineTable({
id: v.string(),
user_id: v.string(),
type: v.string(),
status: v.union(v.literal("queued"), v.literal("running"), v.literal("succeeded"), v.literal("failed")),
payload: v.any(),
result: v.union(v.any(), v.null()),
error: v.union(v.string(), v.null()),
created_at: v.string(),
updated_at: v.string(),
started_at: v.union(v.string(), v.null()),
finished_at: v.union(v.string(), v.null()),
})
.index("by_job_id", ["id"])
.index("by_user", ["user_id"])
.index("by_status", ["status"]),
// 阶段 6:媒体/附件(替代 Supabase Storage + media_assets 表)
media_assets: defineTable({
id: v.string(),
workspace_id: v.string(),
document_id: v.string(),
asset_type: v.string(),
file_url: v.union(v.string(), v.null()),
thumbnail_url: v.union(v.string(), v.null()),
// Convex Filesstorage)定位信息
// 说明:早期阶段 6(MinIO 直存)写入的记录可能没有该字段;允许缺失以便平滑迁移。
storage_id: v.optional(v.union(v.id("_storage"), v.null())),
// S3/MinIO 定位信息
bucket: v.union(v.string(), v.null()),
storage_path: v.union(v.string(), v.null()),
file_name: v.union(v.string(), v.null()),
file_size: v.union(v.number(), v.null()),
mime_type: v.union(v.string(), v.null()),
// OCR 相关(后续再接入)
ocr_text: v.union(v.string(), v.null()),
ocr_status: v.union(v.string(), v.null()),
ocr_payload: v.optional(v.any()),
ocr_strategy: v.optional(v.union(v.string(), v.null())),
// 回收站/清理
deleted_at: v.union(v.string(), v.null()),
deleted_by: v.union(v.string(), v.null()),
purged_at: v.union(v.string(), v.null()),
created_by: v.string(),
created_at: v.string(),
updated_at: v.string(),
})
.index("by_asset_id", ["id"])
.index("by_storage_id", ["storage_id"])
.index("by_workspace", ["workspace_id"])
.index("by_document", ["document_id"])
.index("by_workspace_deleted", ["workspace_id", "deleted_at"]),
// M2:思维导图(替代本地文件 public/documents/<docId>/mindmap*.json + Supabase documents.mindmap_data
// 说明:
// - 业务侧依然以 (document_id, mindmap_id) 作为“定位键”,避免跨页面冲突。
// - id 是一个便于排查的全局唯一串(例如 `${docId}:${mindmapId}`),但不要求前端依赖它。
mindmaps: defineTable({
id: v.string(),
user_id: v.string(),
workspace_id: v.string(),
document_id: v.string(),
mindmap_id: v.string(),
data: v.any(),
created_at: v.string(),
updated_at: v.string(),
deleted_at: v.union(v.string(), v.null()),
deleted_by: v.union(v.string(), v.null()),
})
.index("by_mindmap_id", ["id"])
.index("by_doc_mindmap", ["document_id", "mindmap_id"])
.index("by_workspace", ["workspace_id"])
.index("by_workspace_deleted", ["workspace_id", "deleted_at"])
.index("by_user", ["user_id"]),
// M3:页面引用/反链(替代 Supabase RPCrecord_page_ref、list_backlinks
page_references: defineTable({
id: v.string(),
workspace_id: v.string(),
source_page_id: v.string(),
source_block_id: v.union(v.string(), v.null()),
target_page_id: v.string(),
alias: v.union(v.string(), v.null()),
display_mode: v.union(v.literal("inline"), v.literal("embed")),
is_previewable: v.boolean(),
created_by: v.string(),
created_at: v.string(),
updated_at: v.string(),
})
.index("by_reference_id", ["id"])
.index("by_workspace_target", ["workspace_id", "target_page_id"])
.index("by_workspace_source", ["workspace_id", "source_page_id"])
.index("by_unique", ["workspace_id", "source_page_id", "source_block_id", "target_page_id", "display_mode"]),
});