What is Convex?
Convex 是一个开源的BAAS(Backend as a Service),它不仅提供了一个实时性的数据库,而且还有文件存储,定时任务,等各种服务,还包括诸如Cloudflare R2,Rate Limiter, AI Agent等插件。
Why using it?
首先,最大的好处就是不需要写原生的SQL queries,也不需要ORM,所有的queries都是用TypeScript编写,可以做到端到端类型安全
其次Convex使用Websocket在客户端获取服务端的数据,也就意味着这个数据是实时的,如果数据发生了变化,你不需要做任何的设置,客户端上的数据永远是最新的。
Convex的所有写入操作都是原子化的,也就是说所有的写入操作都是在一个事务(Transaction)里进行,这保证了数据的一致性
Basic Usage
Tips
With Server Component
如果想要实现SSR,则需要使用
fetchQuery
服务端获取数据,但是响应的服务端不支持实时刷新数据import { fetchQuery } from "convex/nextjs"; import { api } from "@/convex/_generated/api"; export async function StaticTasks() { // 如果该query需要验证,则需要传入token const tasks = await fetchQuery(api.tasks.list, { list: "default" }, { token: "your-jwt-token" }); // render `tasks`... return <div>...</div>; }
HTTP Endpoint (usually work with webhooks)
import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; import { internal } from "./_generated/api"; import type { WebhookEvent } from "@clerk/backend"; import { Webhook } from "svix"; const http = httpRouter(); // 设置一个API endpoint http.route({ path: "/clerk-users-webhook", method: "POST", // 注意这里的ctx不能直接访问数据库,只能通过runQuery或runMutation来和数据库互动 handler: httpAction(async (ctx, request) => { const event = await validateRequest(request); if (!event) { return new Response("Error occured", { status: 400 }); } switch (event.type) { case "user.created": case "user.updated": await ctx.runMutation(internal.users.upsertFromClerk, { data: event.data, }); break; case "user.deleted": { const clerkUserId = event.data.id!; await ctx.runMutation(internal.users.deleteFromClerk, { clerkUserId }); break; } default: console.log("Ignored Clerk webhook event", event.type); } return new Response(null, { status: 200 }); }), }); async function validateRequest(req: Request): Promise<WebhookEvent | null> { const payloadString = await req.text(); const svixHeaders = { "svix-id": req.headers.get("svix-id")!, "svix-timestamp": req.headers.get("svix-timestamp")!, "svix-signature": req.headers.get("svix-signature")!, }; const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!); try { return wh.verify(payloadString, svixHeaders) as unknown as WebhookEvent; } catch (error) { console.error("Error verifying webhook event", error); return null; } } export default http;
Invoking side-effect action inside a mutation
正常来说mutation应该是纯函数,不允许有副作用,但是可以通过
scheduler
来实现调用第三方apiexport const mutationThatSchedulesAction = mutation({ args: { text: v.string() }, handler: async (ctx, { text }) => { const taskId = await ctx.db.insert("tasks", { text }); await ctx.scheduler.runAfter(0, internal.myFunctions.actionThatCallsAPI, { taskId, text, }); }, });
Schema
修改表结构
如果要删除一个表的字段,但是已经有数据了,可以先把他变成optional的字段(e.g.,
v.optional(v.string())
),然后把这个字段的所有数据改成undefined
,之后就可以顺利删掉这个字段了添加索引
索引可以极大加快查询速度,但是索引也会占用空间,所以要在schema里避免写多余的索引
// ❌ 这里用到了两个索引 const allTeamMembers = await ctx.db .query("teamMembers") .withIndex("by_team", (q) => q.eq("team", teamId)) .collect(); const currentUserId = /* get current user id from `ctx.auth` */ const currentTeamMember = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", (q) => q.eq("team", teamId).eq("user", currentUserId), ) .unique(); // ✅ 其实一个就可以 // Just don't include a condition on `user` when querying for results on `team` const allTeamMembers = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", (q) => q.eq("team", teamId)) .collect(); const currentUserId = /* get current user id from `ctx.auth` */ const currentTeamMember = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", (q) => q.eq("team", teamId).eq("user", currentUserId), ) .unique();
Streamline repeated work with Convex-Helper
Custom React Hook
可以写一个hook来封装重复的逻辑,如只有在用户authenticated之后,再发请求,避免资源浪费
import { FunctionReference } from "convex/server"; import { OptionalRestArgsOrSkip, useConvexAuth, useQueries, } from "convex/react"; import { makeUseQueryWithStatus } from "convex-helpers/react"; export const useQueryWithStatus = makeUseQueryWithStatus(useQueries); /** * A wrapper around useQueryWithStatus that automatically checks authentication state. * If the user is not authenticated, the query is skipped. */ export function useAuthenticatedQueryWithStatus< Query extends FunctionReference<"query"> >(query: Query, args: OptionalRestArgsOrSkip<Query>[0] | "skip") { const { isAuthenticated } = useConvexAuth(); return useQueryWithStatus(query, isAuthenticated ? args : "skip"); }
或者可以用
<Authenticated></Authenticated>
包裹住authenticated route,然后用useQueryWithStatus
即可Custom Functions
这样就可以扩展
ctx
,写Query/Mutatio你直接有了user
这个对象,以及db也受Row Level Security的保护const userQuery = customQuery( query, customCtx(async (ctx) => { const user = await AuthenticationRequired({ ctx }); const db = wrapDatabaseReader({ user }, ctx.db, rules); return { user, db }; }) ); export async function AuthenticationRequired({ ctx, }: { ctx: QueryCtx | MutationCtx | ActionCtx; }) { const identity = await ctx.auth.getUserIdentity(); if (identity === null) { throw new ConvexError('Not authenticated!'); } return await getUser(ctx) }
Using optional parameter to return necessary data
如代码所示
export const myInfo = userQuery({ args: { includeTeam: v.boolean() }, // here set a flag handler: async (ctx, args) => { const userInfo = { name: ctx.user.name, profPic: ctx.user.profilePic }; if (args.includeTeam) { const team = await ctx.db.get(ctx.user.teamId); return { ...userInfo, teamName: team.name, teamId: team._id }; } return userInfo; } });
Components
Convex Components类似于插件生态,可以为Convex扩展更多的功能
Aggregate
这个组件一般用来做聚合操作,比如说求和。
他的使用场景一般为,你想要计算某张表的所有行的某列之和,但你又不想要Query全部的数据,那么你就可以用这个component来搞笑的做各种聚合计算
const aggregate = new TableAggregate<{ Key: number; DataModel: DataModel; TableName: "mytable"; }>(components.aggregate, { sortKey: (doc) => doc._creationTime, // Allows querying across time ranges. sumValue: (doc) => doc.value, // The value to be used in `.sum` calculations. });
然后每次数据更新的时候,也需要更新aggregate组件
// When you insert into the table, call `aggregate.insert` const id = await ctx.db.insert("mytable", { foo, bar }); const doc = await ctx.db.get(id); await aggregate.insert(ctx, doc!); // If you update a document, use `aggregate.replace` const oldDoc = await ctx.db.get(id); await ctx.db.patch(id, { foo }); const newDoc = await ctx.db.get(id); await aggregate.replace(ctx, oldDoc!, newDoc!); // And if you delete a document, use `aggregate.delete` const oldDoc = await ctx.db.get(id); await ctx.db.delete(id); await aggregate.delete(ctx, oldDoc!);
最后就可以进行各种计算了
// convex/myfunctions.ts // then in your queries and mutations you can do const tableCount = await aggregate.count(ctx); // or any of the other examples listed above.
Self Hosting
On Coolify
services: backend: image: 'ghcr.io/get-convex/convex-backend:latest' volumes: - 'data:/convex/data' environment: - SERVICE_FQDN_BACKEND_3210 - 'INSTANCE_NAME=${INSTANCE_NAME:-self-hosted-convex}' - 'INSTANCE_SECRET=${SERVICE_HEX_32_SECRET}' - 'CONVEX_RELEASE_VERSION_DEV=${CONVEX_RELEASE_VERSION_DEV:-}' - 'ACTIONS_USER_TIMEOUT_SECS=${ACTIONS_USER_TIMEOUT_SECS:-}' - 'CONVEX_CLOUD_ORIGIN=${SERVICE_FQDN_BACKEND_3210}' - 'CONVEX_SITE_ORIGIN=${SERVICE_FQDN_BACKEND_3210}/http' - 'DATABASE_URL=${DATABASE_URL:-}' - 'DISABLE_BEACON=${DISABLE_BEACON:-}' - 'REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-}' - 'CONVEX_SELF_HOSTED_URL=${SERVICE_FQDN_CONVEX_6791}' healthcheck: test: 'curl -f http://127.0.0.1:3210/version' interval: 5s start_period: 5s dashboard: image: 'ghcr.io/get-convex/convex-dashboard:latest' environment: - SERVICE_FQDN_CONVEX_6791 - NEXT_PUBLIC_DEPLOYMENT_URL=$SERVICE_FQDN_BACKEND_3210 depends_on: backend: condition: service_healthy healthcheck: test: 'wget -qO- http://127.0.0.1:6791/' interval: 5s start_period: 5s