convex-components

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Authoring Convex Components

Convex组件编写指南

Overview

概述

Convex components are isolated, reusable backend modules that can be packaged and shared. Each component has its own schema, functions, and namespace, enabling modular architecture and library distribution via NPM.
Convex组件是可打包共享的独立、可复用后端模块。每个组件都有自己的Schema、函数和命名空间,支持模块化架构,并可通过NPM进行库分发。

TypeScript: NEVER Use
any
Type

TypeScript:禁止使用
any
类型

CRITICAL RULE: This codebase has
@typescript-eslint/no-explicit-any
enabled. Using
any
will cause build failures.
关键规则: 此代码库启用了
@typescript-eslint/no-explicit-any
规则。使用
any
会导致构建失败。

When to Use This Skill

适用场景

Use this skill when:
  • Building reusable backend modules for your app
  • Packaging Convex functionality for NPM distribution
  • Creating isolated sub-systems with separate schemas
  • Integrating third-party Convex components
  • Building libraries with Convex backends (rate limiters, workpools, etc.)
在以下场景中使用本指南:
  • 为你的应用构建可复用后端模块
  • 为NPM分发打包Convex功能
  • 创建带有独立Schema的隔离子系统
  • 集成第三方Convex组件
  • 构建基于Convex后端的库(如速率限制器、工作池等)

Component Types

组件类型

TypeUse CaseLocation
LocalPrivate modules within your app
convex/myComponent/
PackagedPublished NPM packages
node_modules/@org/component/
HybridShared internal packages
packages/my-component/
类型适用场景位置
本地组件应用内部的私有模块
convex/myComponent/
打包组件发布为NPM包的组件
node_modules/@org/component/
混合组件内部共享的包
packages/my-component/

Component Anatomy

组件结构

Directory Structure

目录结构

my-component/
├── package.json           # NPM package config
├── src/
│   └── component/
│       ├── convex.config.ts    # Component definition
│       ├── schema.ts           # Component schema
│       ├── public.ts           # Public API functions
│       ├── internal.ts         # Internal functions
│       └── _generated/         # Generated types (gitignored)
└── src/
    └── client/
        └── index.ts            # Client-side wrapper class
my-component/
├── package.json           # NPM包配置
├── src/
│   └── component/
│       ├── convex.config.ts    # 组件定义文件
│       ├── schema.ts           # 组件Schema
│       ├── public.ts           # 公共API函数
│       ├── internal.ts         # 内部函数
│       └── _generated/         # 生成的类型文件(已加入git忽略)
└── src/
    └── client/
        └── index.ts            # 客户端封装类

convex.config.ts

convex.config.ts

The component definition file:
typescript
// src/component/convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("myComponent");
组件定义文件示例:
typescript
// src/component/convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("myComponent");

Component Schema

组件Schema

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

export default defineSchema({
  items: defineTable({
    key: v.string(),
    value: v.string(),
    expiresAt: v.optional(v.number()),
  }).index("by_key", ["key"]),
});
typescript
// src/component/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  items: defineTable({
    key: v.string(),
    value: v.string(),
    expiresAt: v.optional(v.number()),
  }).index("by_key", ["key"]),
});

Public API Functions

公共API函数

typescript
// src/component/public.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

export const set = mutation({
  args: {
    key: v.string(),
    value: v.string(),
    ttl: v.optional(v.number()),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("items")
      .withIndex("by_key", (q) => q.eq("key", args.key))
      .unique();

    const data = {
      key: args.key,
      value: args.value,
      expiresAt: args.ttl ? Date.now() + args.ttl : undefined,
    };

    if (existing) {
      await ctx.db.replace(existing._id, data);
    } else {
      await ctx.db.insert("items", data);
    }

    return null;
  },
});

export const get = query({
  args: { key: v.string() },
  returns: v.union(v.string(), v.null()),
  handler: async (ctx, args) => {
    const item = await ctx.db
      .query("items")
      .withIndex("by_key", (q) => q.eq("key", args.key))
      .unique();

    if (!item) return null;
    if (item.expiresAt && item.expiresAt < Date.now()) return null;

    return item.value;
  },
});
typescript
// src/component/public.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

export const set = mutation({
  args: {
    key: v.string(),
    value: v.string(),
    ttl: v.optional(v.number()),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("items")
      .withIndex("by_key", (q) => q.eq("key", args.key))
      .unique();

    const data = {
      key: args.key,
      value: args.value,
      expiresAt: args.ttl ? Date.now() + args.ttl : undefined,
    };

    if (existing) {
      await ctx.db.replace(existing._id, data);
    } else {
      await ctx.db.insert("items", data);
    }

    return null;
  },
});

export const get = query({
  args: { key: v.string() },
  returns: v.union(v.string(), v.null()),
  handler: async (ctx, args) => {
    const item = await ctx.db
      .query("items")
      .withIndex("by_key", (q) => q.eq("key", args.key))
      .unique();

    if (!item) return null;
    if (item.expiresAt && item.expiresAt < Date.now()) return null;

    return item.value;
  },
});

Installing Components in Host App

在宿主应用中安装组件

Host App convex.config.ts

宿主应用的convex.config.ts

typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";

const app = defineApp();
app.use(myComponent, { name: "cache" }); // Mount with a name

export default app;
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";

const app = defineApp();
app.use(myComponent, { name: "cache" }); // 为组件指定挂载名称

export default app;

Accessing Component Functions

访问组件函数

typescript
// convex/myFunctions.ts
import { mutation } from "./_generated/server";
import { components } from "./_generated/api";
import { v } from "convex/values";

export const cacheValue = mutation({
  args: { key: v.string(), value: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Call component function via components.<name>.<module>.<function>
    await ctx.runMutation(components.cache.public.set, {
      key: args.key,
      value: args.value,
      ttl: 3600000, // 1 hour
    });
    return null;
  },
});
typescript
// convex/myFunctions.ts
import { mutation } from "./_generated/server";
import { components } from "./_generated/api";
import { v } from "convex/values";

export const cacheValue = mutation({
  args: { key: v.string(), value: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // 通过components.<挂载名>.<模块>.<函数>调用组件函数
    await ctx.runMutation(components.cache.public.set, {
      key: args.key,
      value: args.value,
      ttl: 3600000, // 1小时
    });
    return null;
  },
});

Key Differences from Regular Convex

与常规Convex的主要区别

1. Id Types Are Opaque

1. ID类型为不透明类型

Component IDs are branded strings, not
Id<"table">
:
typescript
// ❌ WRONG: Can't use Id<"items"> from component
import { Id } from "./_generated/dataModel";
const id: Id<"items"> = ...;  // Error!

// ✅ CORRECT: Use string or branded type from component
export const processItem = mutation({
  args: { itemId: v.string() },  // Accept as string
  returns: v.null(),
  handler: async (ctx, args) => {
    // Pass to component functions as-is
    await ctx.runMutation(components.myComponent.public.process, {
      itemId: args.itemId,
    });
    return null;
  },
});
组件的ID是带标识的字符串,而非
Id<"table">
类型:
typescript
// ❌ 错误:无法使用组件的Id<"items">类型
import { Id } from "./_generated/dataModel";
const id: Id<"items"> = ...;  // 报错!

// ✅ 正确:使用字符串或组件提供的带标识类型
export const processItem = mutation({
  args: { itemId: v.string() },  // 以字符串形式接收
  returns: v.null(),
  handler: async (ctx, args) => {
    // 直接传递给组件函数
    await ctx.runMutation(components.myComponent.public.process, {
      itemId: args.itemId,
    });
    return null;
  },
});

2. No Environment Variables

2. 无法访问环境变量

Components cannot access
process.env
. Pass configuration explicitly:
typescript
// ✅ CORRECT: Component accepts config via function args
// src/component/public.ts
export const initialize = mutation({
  args: {
    apiKey: v.string(),
    endpoint: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert("config", {
      apiKey: args.apiKey,
      endpoint: args.endpoint,
    });
    return null;
  },
});

// Host app passes env vars
export const setup = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    await ctx.runMutation(components.myComponent.public.initialize, {
      apiKey: process.env.API_KEY!,
      endpoint: process.env.API_ENDPOINT!,
    });
    return null;
  },
});
组件无法访问
process.env
,需显式传递配置:
typescript
// ✅ 正确:组件通过函数参数接收配置
// src/component/public.ts
export const initialize = mutation({
  args: {
    apiKey: v.string(),
    endpoint: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert("config", {
      apiKey: args.apiKey,
      endpoint: args.endpoint,
    });
    return null;
  },
});

// 宿主应用传递环境变量
export const setup = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    await ctx.runMutation(components.myComponent.public.initialize, {
      apiKey: process.env.API_KEY!,
      endpoint: process.env.API_ENDPOINT!,
    });
    return null;
  },
});

3. No ctx.auth

3. 无ctx.auth上下文

Components don't have access to authentication context. Pass user info explicitly:
typescript
// ❌ WRONG: Components can't access ctx.auth
export const createItem = mutation({
  args: { data: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity(); // Error!
    return null;
  },
});

// ✅ CORRECT: Accept userId as argument
export const createItem = mutation({
  args: {
    userId: v.string(),
    data: v.string(),
  },
  returns: v.id("items"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("items", {
      userId: args.userId,
      data: args.data,
    });
  },
});

// Host app handles auth and passes user info
export const createItemWithAuth = mutation({
  args: { data: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthorized");

    const itemId = await ctx.runMutation(
      components.myComponent.public.createItem,
      {
        userId: identity.subject,
        data: args.data,
      }
    );

    return itemId; // Return as string
  },
});
组件无法访问认证上下文,需显式传递用户信息:
typescript
// ❌ 错误:组件无法访问ctx.auth
export const createItem = mutation({
  args: { data: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity(); // 报错!
    return null;
  },
});

// ✅ 正确:接收userId作为参数
export const createItem = mutation({
  args: {
    userId: v.string(),
    data: v.string(),
  },
  returns: v.id("items"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("items", {
      userId: args.userId,
      data: args.data,
    });
  },
});

// 宿主应用处理认证并传递用户信息
export const createItemWithAuth = mutation({
  args: { data: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("未授权");

    const itemId = await ctx.runMutation(
      components.myComponent.public.createItem,
      {
        userId: identity.subject,
        data: args.data,
      }
    );

    return itemId; // 以字符串形式返回
  },
});

4. Pagination Cursors Are Strings

4. 分页游标为字符串类型

Component pagination uses string cursors:
typescript
// src/component/public.ts
export const list = query({
  args: {
    cursor: v.optional(v.string()),
    limit: v.number(),
  },
  returns: v.object({
    items: v.array(
      v.object({
        id: v.string(),
        data: v.string(),
      })
    ),
    nextCursor: v.union(v.string(), v.null()),
  }),
  handler: async (ctx, args) => {
    const results = await ctx.db
      .query("items")
      .paginate({ cursor: args.cursor ?? null, numItems: args.limit });

    return {
      items: results.page.map((item) => ({
        id: item._id,
        data: item.data,
      })),
      nextCursor: results.continueCursor,
    };
  },
});
组件分页使用字符串游标:
typescript
// src/component/public.ts
export const list = query({
  args: {
    cursor: v.optional(v.string()),
    limit: v.number(),
  },
  returns: v.object({
    items: v.array(
      v.object({
        id: v.string(),
        data: v.string(),
      })
    ),
    nextCursor: v.union(v.string(), v.null()),
  }),
  handler: async (ctx, args) => {
    const results = await ctx.db
      .query("items")
      .paginate({ cursor: args.cursor ?? null, numItems: args.limit });

    return {
      items: results.page.map((item) => ({
        id: item._id,
        data: item.data,
      })),
      nextCursor: results.continueCursor,
    };
  },
});

Client Code Patterns

客户端代码模式

Class-Based Client Wrapper

基于类的客户端封装

Provide a convenient client class for host apps:
typescript
// src/client/index.ts
import {
  FunctionReference,
  GenericMutationCtx,
  GenericQueryCtx,
} from "convex/server";
import { api } from "../component/_generated/api";

// Re-export the component config
export { default } from "../component/convex.config";

// Type for the installed component
type ComponentApi = typeof api;

export class MyComponentClient {
  private component: ComponentApi;

  constructor(component: ComponentApi) {
    this.component = component;
  }

  // Wrapper methods for cleaner API
  async set(
    ctx: GenericMutationCtx<Record<string, never>>,
    key: string,
    value: string,
    ttl?: number
  ): Promise<void> {
    await ctx.runMutation(this.component.public.set, { key, value, ttl });
  }

  async get(
    ctx: GenericQueryCtx<Record<string, never>>,
    key: string
  ): Promise<string | null> {
    return await ctx.runQuery(this.component.public.get, { key });
  }
}
为宿主应用提供便捷的客户端类:
typescript
// src/client/index.ts
import {
  FunctionReference,
  GenericMutationCtx,
  GenericQueryCtx,
} from "convex/server";
import { api } from "../component/_generated/api";

// 重新导出组件配置
export { default } from "../component/convex.config";

// 已安装组件的类型
type ComponentApi = typeof api;

export class MyComponentClient {
  private component: ComponentApi;

  constructor(component: ComponentApi) {
    this.component = component;
  }

  // 封装方法以简化API调用
  async set(
    ctx: GenericMutationCtx<Record<string, never>>,
    key: string,
    value: string,
    ttl?: number
  ): Promise<void> {
    await ctx.runMutation(this.component.public.set, { key, value, ttl });
  }

  async get(
    ctx: GenericQueryCtx<Record<string, never>>,
    key: string
  ): Promise<string | null> {
    return await ctx.runQuery(this.component.public.get, { key });
  }
}

Host App Usage

宿主应用使用示例

typescript
// convex/cache.ts
import { MyComponentClient } from "@org/my-component";
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

const cache = new MyComponentClient(components.cache);

export const setValue = mutation({
  args: { key: v.string(), value: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    await cache.set(ctx, args.key, args.value, 3600000);
    return null;
  },
});

export const getValue = query({
  args: { key: v.string() },
  returns: v.union(v.string(), v.null()),
  handler: async (ctx, args) => {
    return await cache.get(ctx, args.key);
  },
});
typescript
// convex/cache.ts
import { MyComponentClient } from "@org/my-component";
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

const cache = new MyComponentClient(components.cache);

export const setValue = mutation({
  args: { key: v.string(), value: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    await cache.set(ctx, args.key, args.value, 3600000);
    return null;
  },
});

export const getValue = query({
  args: { key: v.string() },
  returns: v.union(v.string(), v.null()),
  handler: async (ctx, args) => {
    return await cache.get(ctx, args.key);
  },
});

Building & Publishing

构建与发布

package.json Setup

package.json配置

json
{
  "name": "@org/my-component",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/client/index.js",
  "types": "./dist/client/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/client/index.d.ts",
      "import": "./dist/client/index.js"
    },
    "./convex.config": {
      "types": "./dist/component/convex.config.d.ts",
      "import": "./dist/component/convex.config.js"
    }
  },
  "files": ["dist", "src"],
  "scripts": {
    "build": "npm run build:esm && npm run build:cjs",
    "build:esm": "tsc --project tsconfig.json",
    "typecheck": "tsc --noEmit",
    "prepare": "npm run build"
  },
  "dependencies": {
    "convex": "^1.17.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  },
  "peerDependencies": {
    "convex": "^1.17.0"
  }
}
json
{
  "name": "@org/my-component",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/client/index.js",
  "types": "./dist/client/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/client/index.d.ts",
      "import": "./dist/client/index.js"
    },
    "./convex.config": {
      "types": "./dist/component/convex.config.d.ts",
      "import": "./dist/component/convex.config.js"
    }
  },
  "files": ["dist", "src"],
  "scripts": {
    "build": "npm run build:esm && npm run build:cjs",
    "build:esm": "tsc --project tsconfig.json",
    "typecheck": "tsc --noEmit",
    "prepare": "npm run build"
  },
  "dependencies": {
    "convex": "^1.17.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  },
  "peerDependencies": {
    "convex": "^1.17.0"
  }
}

tsconfig.json

tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Generate Types Before Publishing

发布前生成类型文件

bash
undefined
bash
undefined

In component directory

进入组件目录

cd src/component npx convex codegen
cd src/component npx convex codegen

Build the package

构建包

cd ../.. npm run build
cd ../.. npm run build

Publish

发布

npm publish
undefined
npm publish
undefined

Testing Components

组件测试

Using convex-test

使用convex-test

typescript
// tests/component.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";

describe("MyComponent", () => {
  it("should set and get values", async () => {
    const t = convexTest(schema);

    // Set a value
    await t.mutation(api.public.set, {
      key: "test-key",
      value: "test-value",
    });

    // Get the value
    const result = await t.query(api.public.get, {
      key: "test-key",
    });

    expect(result).toBe("test-value");
  });

  it("should respect TTL expiration", async () => {
    const t = convexTest(schema);

    // Set with TTL of 0 (immediate expiry)
    await t.mutation(api.public.set, {
      key: "expiring-key",
      value: "expiring-value",
      ttl: -1000, // Already expired
    });

    // Should return null for expired item
    const result = await t.query(api.public.get, {
      key: "expiring-key",
    });

    expect(result).toBeNull();
  });
});
typescript
// tests/component.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";

describe("MyComponent", () => {
  it("应该能设置并获取值", async () => {
    const t = convexTest(schema);

    // 设置值
    await t.mutation(api.public.set, {
      key: "test-key",
      value: "test-value",
    });

    // 获取值
    const result = await t.query(api.public.get, {
      key: "test-key",
    });

    expect(result).toBe("test-value");
  });

  it("应该遵守TTL过期规则", async () => {
    const t = convexTest(schema);

    // 设置TTL为0(立即过期)
    await t.mutation(api.public.set, {
      key: "expiring-key",
      value: "expiring-value",
      ttl: -1000, // 已过期
    });

    // 过期项应返回null
    const result = await t.query(api.public.get, {
      key: "expiring-key",
    });

    expect(result).toBeNull();
  });
});

Testing with Mock Data

使用模拟数据测试

typescript
// tests/with-data.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";

describe("MyComponent with data", () => {
  it("should handle existing data", async () => {
    const t = convexTest(schema);

    // Seed data directly
    await t.run(async (ctx) => {
      await ctx.db.insert("items", {
        key: "seeded-key",
        value: "seeded-value",
      });
    });

    // Query should find seeded data
    const result = await t.query(api.public.get, {
      key: "seeded-key",
    });

    expect(result).toBe("seeded-value");
  });
});
typescript
// tests/with-data.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";

describe("带数据的MyComponent", () => {
  it("应该能处理已有数据", async () => {
    const t = convexTest(schema);

    // 直接植入测试数据
    await t.run(async (ctx) => {
      await ctx.db.insert("items", {
        key: "seeded-key",
        value: "seeded-value",
      });
    });

    // 查询应能找到植入的数据
    const result = await t.query(api.public.get, {
      key: "seeded-key",
    });

    expect(result).toBe("seeded-value");
  });
});

Local Component Pattern

本地组件模式

For app-internal modularity without NPM publishing:
convex/
├── convex.config.ts           # Main app config
├── schema.ts                  # Main app schema
├── myLocalComponent/          # Local component
│   ├── convex.config.ts       # Component definition
│   ├── schema.ts              # Component schema
│   └── functions.ts           # Component functions
└── functions.ts               # Main app functions
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myLocalComponent from "./myLocalComponent/convex.config";

const app = defineApp();
app.use(myLocalComponent, { name: "localCache" });

export default app;
无需发布到NPM,仅用于应用内部模块化:
convex/
├── convex.config.ts           # 主应用配置
├── schema.ts                  # 主应用Schema
├── myLocalComponent/          # 本地组件
│   ├── convex.config.ts       # 组件定义
│   ├── schema.ts              # 组件Schema
│   └── functions.ts           # 组件函数
└── functions.ts               # 主应用函数
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myLocalComponent from "./myLocalComponent/convex.config";

const app = defineApp();
app.use(myLocalComponent, { name: "localCache" });

export default app;

Common Pitfalls

常见陷阱

Pitfall 1: Using Id Types from Component

陷阱1:使用组件的Id类型

❌ WRONG:
typescript
import { Id } from "./_generated/dataModel";

export const getItem = query({
  args: { itemId: v.id("items") }, // ❌ Not accessible!
  handler: async (ctx, args) => {
    return await ctx.db.get(args.itemId);
  },
});
✅ CORRECT:
typescript
export const getItem = query({
  args: { itemId: v.string() }, // ✅ Accept as string
  returns: v.union(
    v.object({
      id: v.string(),
      data: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    // Look up by index or iterate
    const item = await ctx.db
      .query("items")
      .filter((q) => q.eq(q.field("_id"), args.itemId))
      .first();

    if (!item) return null;
    return { id: item._id, data: item.data };
  },
});
❌ 错误:
typescript
import { Id } from "./_generated/dataModel";

export const getItem = query({
  args: { itemId: v.id("items") }, // ❌ 无法访问!
  handler: async (ctx, args) => {
    return await ctx.db.get(args.itemId);
  },
});
✅ 正确:
typescript
export const getItem = query({
  args: { itemId: v.string() }, // ✅ 以字符串形式接收
  returns: v.union(
    v.object({
      id: v.string(),
      data: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    // 通过索引查询或遍历查找
    const item = await ctx.db
      .query("items")
      .filter((q) => q.eq(q.field("_id"), args.itemId))
      .first();

    if (!item) return null;
    return { id: item._id, data: item.data };
  },
});

Pitfall 2: Accessing process.env in Component

陷阱2:在组件中访问process.env

❌ WRONG:
typescript
// src/component/functions.ts
export const callApi = action({
  args: {},
  returns: v.null(),
  handler: async () => {
    // ❌ Components can't access process.env!
    const apiKey = process.env.API_KEY;
    await fetch("...", { headers: { Authorization: apiKey } });
    return null;
  },
});
✅ CORRECT:
typescript
// src/component/functions.ts
export const callApi = action({
  args: { apiKey: v.string() }, // ✅ Accept as argument
  returns: v.null(),
  handler: async (ctx, args) => {
    await fetch("...", { headers: { Authorization: args.apiKey } });
    return null;
  },
});

// Host app passes env var
await ctx.runAction(components.myComponent.functions.callApi, {
  apiKey: process.env.API_KEY!,
});
❌ 错误:
typescript
// src/component/functions.ts
export const callApi = action({
  args: {},
  returns: v.null(),
  handler: async () => {
    // ❌ 组件无法访问process.env!
    const apiKey = process.env.API_KEY;
    await fetch("...", { headers: { Authorization: apiKey } });
    return null;
  },
});
✅ 正确:
typescript
// src/component/functions.ts
export const callApi = action({
  args: { apiKey: v.string() }, // ✅ 通过参数接收
  returns: v.null(),
  handler: async (ctx, args) => {
    await fetch("...", { headers: { Authorization: args.apiKey } });
    return null;
  },
});

// 宿主应用传递环境变量
await ctx.runAction(components.myComponent.functions.callApi, {
  apiKey: process.env.API_KEY!,
});

Pitfall 3: Expecting ctx.auth in Component

陷阱3:期望组件拥有ctx.auth

❌ WRONG:
typescript
// src/component/functions.ts
export const userAction = mutation({
  args: { data: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ Components don't have ctx.auth!
    const identity = await ctx.auth.getUserIdentity();
    return null;
  },
});
✅ CORRECT:
typescript
// src/component/functions.ts
export const userAction = mutation({
  args: {
    userId: v.string(), // ✅ Accept user info as argument
    data: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert("userActions", {
      userId: args.userId,
      data: args.data,
    });
    return null;
  },
});

// Host app handles auth
export const doUserAction = mutation({
  args: { data: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthorized");

    await ctx.runMutation(components.myComponent.functions.userAction, {
      userId: identity.subject,
      data: args.data,
    });
    return null;
  },
});
❌ 错误:
typescript
// src/component/functions.ts
export const userAction = mutation({
  args: { data: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ 组件没有ctx.auth!
    const identity = await ctx.auth.getUserIdentity();
    return null;
  },
});
✅ 正确:
typescript
// src/component/functions.ts
export const userAction = mutation({
  args: {
    userId: v.string(), // ✅ 通过参数接收用户信息
    data: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert("userActions", {
      userId: args.userId,
      data: args.data,
    });
    return null;
  },
});

// 宿主应用处理认证
export const doUserAction = mutation({
  args: { data: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("未授权");

    await ctx.runMutation(components.myComponent.functions.userAction, {
      userId: identity.subject,
      data: args.data,
    });
    return null;
  },
});

Quick Reference

快速参考

Component Definition

组件定义

typescript
// convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("componentName");
typescript
// convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("componentName");

Host App Installation

宿主应用安装

typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";

const app = defineApp();
app.use(myComponent, { name: "mountName" });
export default app;
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";

const app = defineApp();
app.use(myComponent, { name: "mountName" });
export default app;

Calling Component Functions

调用组件函数

typescript
import { components } from "./_generated/api";

// In mutations/queries/actions
await ctx.runMutation(components.mountName.module.function, args);
await ctx.runQuery(components.mountName.module.function, args);
await ctx.runAction(components.mountName.module.function, args);
typescript
import { components } from "./_generated/api";

// 在mutation/query/action中调用
await ctx.runMutation(components.mountName.module.function, args);
await ctx.runQuery(components.mountName.module.function, args);
await ctx.runAction(components.mountName.module.function, args);

Component Limitations

组件限制

FeatureAvailable in Components?
ctx.db
Yes (own schema only)
ctx.auth
No
process.env
No
ctx.scheduler
Yes
Id<"table">
types
No (use strings)
Pagination cursorsString only
功能特性组件中是否可用?
ctx.db
是(仅能访问自身Schema)
ctx.auth
process.env
ctx.scheduler
Id<"table">
类型
否(使用字符串)
分页游标仅支持字符串类型