返回技能市场
开发运维 安全

convex-create-component

@admin/convex-create-component

Builds reusable Convex components with isolated tables and app-facing APIs. Use for new components, reusable backend modules, integrations, or component boundary work.

admin 热度 275v0.0.1

创建 Convex 组件

创建具有清晰边界和小型面向应用 API 的可复用 Convex 组件。

何时使用

  • 在现有应用中创建新的 Convex 组件
  • 将可复用后端逻辑提取到组件中
  • 构建应当拥有自身表和工作流的第三方集成
  • 打包 Convex 功能以便在多个应用中复用

何时不使用

  • 属于主应用的一次性业务逻辑
  • 不需要 Convex 表或函数的薄工具函数
  • 应保留在 convex/ 中的应用级编排
  • 普通 TypeScript 库已经足够的场景

工作流

  1. 询问用户正在构建什么以及最终目标是什么。如果仓库已经使答案显而易见,请说明这一点,并在继续之前确认。
  2. 使用下面的决策树选择形态,并阅读匹配的参考文件。
  3. 判断是否有必要使用组件。如果功能不需要隔离的表、后端函数或可复用持久状态,优先使用普通应用代码或普通库。
  4. 为以下内容制定简短计划:
  • 组件拥有哪些表
  • 它暴露哪些公共函数
  • 必须从应用传入哪些数据(认证、环境变量、父级 ID)
  • 哪些内容作为包装器或 HTTP 挂载保留在应用中
  1. 使用 convex.config.tsschema.ts 和函数文件创建组件结构。
  2. 使用组件自身的 ./_generated/server 导入实现函数,而不是应用的生成文件。
  3. 使用 app.use(...) 将组件接入应用。如果应用尚没有 convex/convex.config.ts,请创建它。
  4. 在应用中使用 ctx.runQueryctx.runMutationctx.runAction 通过 components.<name> 调用组件。
  5. 如果 React 客户端、HTTP 调用方或公共 API 需要访问,请在应用中创建包装函数,而不是直接暴露组件函数。
  6. 运行 npx convex dev,并在完成之前修复代码生成、类型或边界问题。

选择形态

询问用户,然后选择一条路径:

| 目标 | 形态 | 参考 | | ------------------------------------------------- | ---------------- | ----------------------------------- | | 仅用于此应用的组件 | 本地 | references/local-components.md | | 跨应用发布或共享 | 打包 | references/packaged-components.md | | 用户明确需要本地 + 共享库代码 | 混合 | references/hybrid-components.md | | 不确定 | 默认本地 | references/local-components.md |

在继续之前只读取一个参考文件。

默认方式

除非用户明确需要 npm 包,否则默认使用本地组件:

  • 将其放在 convex/components/<componentName>/
  • 在其自身的 convex.config.ts 中使用 defineComponent(...) 定义它
  • 从应用的 convex/convex.config.ts 使用 app.use(...) 安装它
  • npx convex dev 生成组件自身的 _generated/ 文件

组件骨架

一个包含一个表和两个函数的最简本地组件,以及应用接线。

// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("notifications");
// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  notifications: defineTable({
    userId: v.string(),
    message: v.string(),
    read: v.boolean(),
  }).index("by_user_read", ["userId", "read"]),
});
// convex/components/notifications/lib.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";

export const send = mutation({
  args: { userId: v.string(), message: v.string() },
  returns: v.id("notifications"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("notifications", {
      userId: args.userId,
      message: args.message,
      read: false,
    });
  },
});

export const listUnread = query({
  args: { userId: v.string() },
  returns: v.array(
    v.object({
      _id: v.id("notifications"),
      _creationTime: v.number(),
      userId: v.string(),
      message: v.string(),
      read: v.boolean(),
    }),
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("notifications")
      .withIndex("by_user_read", (q) =>
        q.eq("userId", args.userId).eq("read", false),
      )
      .collect();
  },
});
// convex/convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config.js";

const app = defineApp();
app.use(notifications);

export default app;
// convex/notifications.ts  (app-side wrapper)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { getAuthUserId } from "@convex-dev/auth/server";

export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});

export const myUnread = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    return await ctx.runQuery(components.notifications.lib.listUnread, {
      userId,
    });
  },
});

注意参考路径形态:convex/components/notifications/lib.ts 中的函数在应用中通过 components.notifications.lib.send 调用。

关键规则

  • 将认证保留在应用中,因为组件内部无法使用 ctx.auth
  • 将环境访问保留在应用中,因为组件函数无法读取 process.env
  • 将父应用 ID 作为字符串跨边界传递,因为在面向应用的 ComponentApi 中,Id 类型会变成普通字符串。
  • 不要在组件参数或 schema 中对应用拥有的表使用 v.id("parentTable"),因为组件无法访问应用的表命名空间。
  • 从组件自身的 ./_generated/server 导入 querymutationaction,而不是应用的生成文件。
  • 不要直接向客户端暴露组件函数。当需要客户端访问时,请创建应用包装器,因为组件是内部的,并且需要应用提供的认证/环境接线。
  • 如果组件定义了 HTTP 处理程序,请在应用的 convex/http.ts 中挂载路由,因为组件无法注册自己的 HTTP 路由。
  • 如果组件需要分页,请使用来自 convex-helperspaginator,而不是内置的 .paginate(),因为 .paginate() 无法跨组件边界工作。
  • 为被查询字段定义索引,而不是在数据库查询之后使用 Convex .filter()
  • 为所有公共组件函数添加 argsreturns 验证器,因为组件边界需要显式类型契约。

模式

认证和环境访问

// Bad: component code cannot rely on app auth or env
const identity = await ctx.auth.getUserIdentity();
const apiKey = process.env.OPENAI_API_KEY;
// Good: the app resolves auth and env, then passes explicit values
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");

await ctx.runAction(components.translator.translate, {
  userId,
  apiKey: process.env.OPENAI_API_KEY,
  text: args.text,
});

面向客户端的 API

// Bad: assuming a component function is directly callable by clients
export const send = components.notifications.send;
// Good: re-export through an app mutation or query
export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});

跨边界的 ID

// Bad: parent app table IDs are not valid component validators
args: {
  userId: v.id("users"),
}
// Good: treat parent-owned IDs as strings at the boundary
args: {
  userId: v.string(),
}

高级模式

有关其他模式,包括用于回调的函数句柄、从 schema 派生验证器、使用 globals 表进行静态配置,以及基于类的客户端包装器,请参见 references/advanced-patterns.md

验证

按此顺序尝试验证:

  1. npx convex codegen --component-dir convex/components/<name>
  2. npx convex codegen
  3. npx convex dev

重要:

  • 新仓库在配置 CONVEX_DEPLOYMENT 之前可能会使这些命令失败。
  • 在代码生成运行之前,组件本地的 ./_generated/* 导入和应用端的 components.<name>... 引用无法通过类型检查。
  • 如果验证因 Convex 登录或部署设置而阻塞,请停止并询问用户该确切步骤,而不是猜测。

参考文件

在用户确认目标后,只读取以下文件中的一个:

  • references/local-components.md
  • references/packaged-components.md
  • references/hybrid-components.md

官方文档: 编写组件

检查清单

  • [ ] 询问用户想要构建什么并确认形态
  • [ ] 阅读匹配的参考文件
  • [ ] 确认组件是正确的抽象
  • [ ] 规划了表、公共 API、边界和应用包装器
  • [ ] 组件位于 convex/components/<name>/ 下(如果发布,则使用包布局)
  • [ ] 组件从自身的 ./_generated/server 导入
  • [ ] 认证、环境访问和 HTTP 路由保留在应用中
  • [ ] 父应用 ID 作为 v.string() 跨边界传递
  • [ ] 公共函数具有 argsreturns 验证器
  • [ ] 已运行 npx convex dev 并修复代码生成或类型问题
qianwen skills install @admin/convex-create-component