mcpField
C
convex-schema-validator

convex-schema-validator

github.com/waynesutton数据与数据库4053.0k 次安装4 周前更新
代码仓库

关于 convex-schema-validator

面向 Convex 开发者的 schema 建模参考,说明字段验证器、文档关系与索引规则,帮助 schema 通过现有数据验证并匹配查询路径。

主要功能

  1. 01字段验证器与类型映射
  2. 02可选值与 null 的区别
  3. 03联合类型建模
  4. 04按查询字段定义索引
  5. 05表关系与嵌入取舍
  6. 06常见 schema 错误检查

适用场景

  1. 01新建表或为现有表添加字段
  2. 02为 withIndex 查询补充索引
  3. 03选择嵌入对象或使用 v.id
  4. 04排查 npx convex dev 验证错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-schema-validator

安装后在 Claude Code 中输入/convex-schema-validator确认已加载。

附带文件agents/openai.yamlassets/small-logo.svg

详细介绍

Convex 架构验证器

生成 convex/schema.ts,为每个文档指定类型、为每条查询路径建立索引,并通过对数据库现有数据的验证。规则只有一条:函数中的每个 withIndex 都需要在此处有匹配的 .index(),索引名根据字段命名,查询字段顺序必须一致。

何时使用

  • 创建新表或为现有表添加字段
  • 查询使用了 .filter(),需要改用索引
  • 决定是嵌入对象还是使用 v.id 关联
  • 为存在多种形态的文档建模
  • npx convex dev 因架构验证错误而失败

架构骨架

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    avatarUrl: v.optional(v.string()),
  }).index("by_email", ["email"]),

  tasks: defineTable({
    userId: v.id("users"),
    title: v.string(),
    completed: v.boolean(),
    priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
  })
    .index("by_userId", ["userId"])
    .index("by_userId_and_completed", ["userId", "completed"]),
});

defineTable 接收字段验证器对象,或单个由 v.object 验证器组成的 v.union(参见判别联合)。每张表都会自动获得 _id 和 _creationTime。不要声明它们。

验证器

验证器TypeScript 类型备注
v.string()stringUTF-8,长度小于 1 MB
v.number()numberFloat64。用于时间戳和计数
v.boolean()boolean
v.null()nullundefined 不是 Convex 值。请改为返回 null
v.int64()bigint不是已弃用的 v.bigint()
v.bytes()ArrayBuffer长度小于 1 MB
v.id("table")Id<"table">类型化引用。Convex 不会验证目标是否存在
v.array(t)T[]最多 8192 个项目
v.object({...}){...}最多 1024 个条目。键不能以 $ 或 _ 开头
v.record(k, t)Record<K, T>动态 ASCII 键。不支持 v.map 或 v.set
v.union(a, b)A | B枚举成员请使用 v.literal
v.literal("x")"x"
v.optional(t)T | undefined字段可以缺失
v.any()any最后手段。会失去类型安全和验证

可选与可空

v.optional 表示文档中可以缺少该键。v.union(t, v.null()) 表示该键始终存在,且其值可以是 null。两者在验证时并不相同。

items: defineTable({
  description: v.optional(v.string()),           // may be absent
  deletedAt: v.union(v.number(), v.null()),       // always present, may be null
  notes: v.optional(v.union(v.string(), v.null())), // either
}),

对于表已有数据后添加的字段,请使用 v.optional。如果“明确清空”本身具有意义,请使用 v.union(..., v.null())。

判别联合

对于文档存在多种形态的表,将由 v.object 验证器组成的 v.union 传给 defineTable。每个成员都有相同的字面量键,TypeScript 会据此进行类型收窄。

events: defineTable(
  v.union(
    v.object({
      kind: v.literal("signup"),
      userId: v.id("users"),
      email: v.string(),
    }),
    v.object({
      kind: v.literal("purchase"),
      userId: v.id("users"),
      orderId: v.id("orders"),
      amount: v.number(),
    }),
  ),
).index("by_kind", ["kind"]),

在联合表的任何索引中,都要将判别字段(kind)放在首位,以便查询限定为一种形态。相比使用一个充满 v.optional 字段的宽泛对象,优先采用这种方式。

索引

命名和字段顺序

按字段顺序命名索引:by_field1_and_field2。查询也必须遵循相同顺序:对字段前缀进行相等比较,然后至多对下一个字段进行一次范围比较。

messages: defineTable({
  channelId: v.id("channels"),
  authorId: v.id("users"),
  sentAt: v.number(),
})
  .index("by_channelId", ["channelId"])
  .index("by_channelId_and_authorId", ["channelId", "authorId"])
  .index("by_channelId_and_sentAt", ["channelId", "sentAt"]),
// Valid: equality on channelId, range on sentAt
await ctx.db
  .query("messages")
  .withIndex("by_channelId_and_sentAt", (q) =>
    q.eq("channelId", args.channelId).gt("sentAt", args.since),
  )
  .order("desc")
  .take(50);

使用该索引时,不能跳过 channelId,只按 sentAt 进行筛选。如果存在这种查询,请添加 by_sentAt。每个索引都会自动追加 _creationTime,因此具有相同字段前缀的结果会按创建时间排序。

保留名称:by_id 和 by_creation_time。限制:每张表最多 32 个索引,每个索引最多 16 个字段。

何时添加索引
  • 函数传给 withIndex、.eq 或范围比较的任何字段
  • 子表上的每个外键(userId、channelId、orgId)
  • 分页列表的排序字段,并在其前面加上范围限定字段
  • 仅在获取文档后读取的字段不需要索引
  • 对于 .collect() 后在内存中筛选即可的小表,不需要索引

如果查询使用了 .filter(),这就表明应添加索引并改用 withIndex。

关系

在子文档上使用 v.id("table") 关联文档。不要在父文档中嵌套会不断增长的对象数组。

// Good: one to many via a foreign key
posts: defineTable({
  authorId: v.id("users"),
  title: v.string(),
}).index("by_authorId", ["authorId"]),

comments: defineTable({
  postId: v.id("posts"),
  authorId: v.id("users"),
  body: v.string(),
}).index("by_postId", ["postId"]),

// Many to many via a join table
postTags: defineTable({
  postId: v.id("posts"),
  tagId: v.id("tags"),
})
  .index("by_postId", ["postId"])
  .index("by_tagId", ["tagId"]),

只有当数据有界、始终与父文档一起加载且一同更新时,才使用 v.object 或较小的 v.array 嵌入数据。用户的 settings 对象适合嵌入。用户的 posts 数组则不适合:它会触及 8192 个项目的上限,而且每次编辑帖子都会重写用户文档。

系统字段

每个文档都包含 _id: Id<"table"> 和 _creationTime: number(自 epoch 起的毫秒数)。如果函数返回完整文档,请在返回值验证器中包含这些字段:

returns: v.array(
  v.object({
    _id: v.id("tasks"),
    _creationTime: v.number(),
    userId: v.id("users"),
    title: v.string(),
    completed: v.boolean(),
  }),
),

除非你需要与插入时间不同的值,否则不要自行添加 createdAt。

搜索和向量索引

像常规索引一样在表上声明。filterFields 必须是顶层字段。

articles: defineTable({
  title: v.string(),
  body: v.string(),
  category: v.string(),
  embedding: v.array(v.number()),
})
  .searchIndex("search_body", {
    searchField: "body",
    filterFields: ["category"],
  })
  .vectorIndex("by_embedding", {
    vectorField: "embedding",
    dimensions: 1536,
    filterFields: ["category"],
  }),

在查询中使用 withSearchIndex 查询搜索索引。向量搜索只能在操作中通过 ctx.vectorSearch 运行。

常见错误

错误原因应改为
withIndex("by_userId") 没有匹配的 .index()部署失败先在架构中声明索引
在 ["userId", "status"] 上将索引命名为 by_user无法明确索引覆盖范围,容易误用by_userId_and_status
在 by_userId_and_status 上查询 status,却未指定 userId索引前缀规则添加 by_status 或包含 userId
为含有行的表添加 newField: v.string()现有文档无法通过验证使用 v.optional,回填数据,然后设为必填
在 users 上使用 posts: v.array(v.object(...))达到 8192 上限,每次编辑都会产生写入冲突拆分为独立的 posts 表,并添加 by_authorId
v.bigint()已弃用v.int64()
在 defineTable 中声明 _id 或 _creationTime不允许这些字段会自动添加
为了快速开发而使用 v.any()没有验证,也没有类型定义数据结构,或使用由实际情况组成的 v.union
为新字段使用 v.union(v.string(), v.null())旧文档完全没有这个键使用 v.optional(v.string())
存储 Date不是 Convex 值使用 v.number() 毫秒时间戳

检查清单

  • 架构位于 convex/schema.ts,并将 defineSchema(...) 作为默认导出
  • 每个表都有显式的字段验证器,除非有充分理由,否则不使用 v.any()
  • convex/ 中的每个 withIndex 调用都有匹配的 .index(),且名称包含字段
  • 外键使用 v.id("table"),并在子表上建立索引
  • 父文档中没有无界的对象数组
  • 为含有数据的表添加的字段使用 v.optional
  • 枚举和多态数据结构使用由 v.literal 或 v.object 成员组成的 v.union
  • 返回完整文档时,返回值验证器包含 _id 和 _creationTime
  • npx convex dev 推送成功,且没有架构验证错误

文档

评论

支持 Markdown