Convex 积分删除
用于管理串联的 Convex 组件删除相关文档。 通过现有的索引配置关系,然后安全地删除知道所有相关记录的文档将自动清理,并有明确的一致性保障.
为什么使用此组件 ?
- 现有计划 - 不向特殊计划定义迁移;使用您现有的
defineTable和指数 - 明确配置 - 在一个地方界定的级联关系的明确、申报性规则
- 两种删除模式 - 内置(原子、单项交易)小项,分批(排定)大树
- 进展跟踪 - 实时批次删除进度的反钩并作被动更新
- 警卫 - 可选
patchDb帮助防止意外直接db.delete电话 - 索引验证 - 启动时抓取配置错误,而不是删除时
- 循环处理 - 通过访问的一组自动处理循环和钻石依赖
- 完全可观察性 - 按表格列出的返回删除摘要
- 非侵入性 - 滴入组件,该组件不会替换您的系统构建器,或需要修改代码,超出删除调用范围
预先要求: Convex
你需要一个已有的 Convex 项目来使用这个组件. Convex是一个主机后端平台,包括一个数据库,没有服务器的功能,还有一堆你还可以了解的 这里。 。 。 .
运行 npm create convex 或遵循其中任何一条 Convex 快速启动 来设置一个.
安装
步骤1:安装软件包
npm install @00akshatsinha00/convex-cascading-delete
步骤2: 将组件添加到您的 Convex 应用程序
// convex/convex.config.ts
import { defineApp } from "convex/server";
import convexCascadingDelete from "@00akshatsinha00/convex-cascading-delete/convex.config";
const app = defineApp();
app.use(convexCascadingDelete);
export default app;
第3步:配置级联规则和即时处理
// convex/cascading.ts
import {
CascadingDelete,
defineCascadeRules,
makeBatchDeleteHandler
} from "@00akshatsinha00/convex-cascading-delete";
import { components } from "./_generated/api";
import { internalMutation } from "./_generated/server";
export const cascadeRules = defineCascadeRules({
users: [
{ to: "posts", via: "byAuthorId", field: "authorId" },
{ to: "comments", via: "byAuthorId", field: "authorId" }
],
posts: [
{ to: "comments", via: "byPostId", field: "postId" }
]
});
export const cd = new CascadingDelete(components.convexCascadingDelete, {
rules: cascadeRules
});
// Required for batched mode - exports an internal mutation that processes deletion batches
export const _cascadeBatchHandler = makeBatchDeleteHandler(
internalMutation,
components.convexCascadingDelete
);
快速启动
使用配置 cd 在您的突变中:
// convex/users.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { cd } from "./cascading";
export const deleteUser = mutation({
args: { userId: v.id("users") },
handler: async (ctx, { userId }) => {
// Deletes user + all their posts + all comments on those posts
const summary = await cd.deleteWithCascade(ctx, "users", userId);
console.log("Deleted:", summary);
// Returns: { users: 1, posts: 5, comments: 23 }
}
});
对于大删除树,使用分批模式:
// convex/organizations.ts
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import { cd } from "./cascading";
export const deleteOrganization = mutation({
args: { orgId: v.id("organizations") },
handler: async (ctx, { orgId }) => {
const result = await cd.deleteWithCascadeBatched(
ctx,
"organizations",
orgId,
{
batchHandlerRef: internal.cascading._cascadeBatchHandler,
batchSize: 2000
}
);
// result.jobId can be used to track progress via useDeletionJobStatus hook
// result.initialSummary contains counts from the first inline batch
return result;
}
});
API 参考
defineCascadeRules(config)
定义和验证表格之间的级联关系。 返回被冻结的配置对象.
const rules = defineCascadeRules({
[sourceTable: string]: [
{
to: string, // Target table name to cascade to
via: string, // Index name on target table
field: string // Field in index used for equality matching (holds parent ID)
}
]
});
所需经费:
- 指定的索引
via目标表中必须存在 - 索引必须包含
field - 这个
field必须包含来自源表格的ID
进行验证:
- 所有属性( A)
to, (中文(简体) ),via, (中文(简体) ),field)必须是在场的和弦 - 复制规则(相同的)
to:via:field组合)被拒绝 - 配置必须是非无效对象
CascadingDelete 类
用于删除操作的主要接口.
构造器
const cd = new CascadingDelete(components.convexCascadingDelete, { rules });
| 参数 | 类型 | 说明 |
|---|---|---|
component | ComponentApi | 构成部分参考来源 components.convexCascadingDelete |
options.rules | CascadeConfig | 规则从 defineCascadeRules() |
deleteWithCascade(ctx, table, id)
删除单项交易中的文档及其所有连带依赖。 使用深度第一的后序转录(在父母之前删除儿童),并使用访问器进行循环检测.
const summary: DeletionSummary = await cd.deleteWithCascade(ctx, "users", userId);
// Returns: { users: 1, posts: 5, comments: 23 }
| 参数 | 类型 | 说明 |
|---|---|---|
ctx | MutationCtx | Convex 变异上下文 |
table | string | 源表格名称 |
id | string | 要删除的文档 ID |
| 回返 | DeletionSummary | 删除文档计数的表格名称地图 |
最佳服务: 中小删除树(4 000多份文件)
一致性 : 完全原子 - 单个 Convex 交易中所有删除成功或全部失败
deleteWithCascadeBatched(ctx, table, id, options)
删除文档及其依存于多个批次交易中。 首先通过只读的 traversal 收集所有目标,删除第一批内入,再通过组件的工作系统将剩余批次排出.
const result = await cd.deleteWithCascadeBatched(
ctx,
"organizations",
orgId,
{
batchHandlerRef: internal.cascading._cascadeBatchHandler,
batchSize: 2000 // Optional, defaults to 2000
}
);
// Returns: { jobId: "j57a...", initialSummary: { organizations: 1, teams: 3 } }
// jobId is null if all targets fit in the first batch
| 参数 | 类型 | 说明 |
|---|---|---|
ctx | MutationCtx | Convex 变异上下文 |
table | string | 源表格名称 |
id | string | 要删除的文档 ID |
options.batchHandlerRef | FunctionReference<"mutation"> | 引用您的导出批量处理器( 从 makeBatchDeleteHandler(中文(简体) ) |
options.batchSize | number (可选) | 每批文件,默认为2000年 |
| 回返 | { jobId: string | null, initialSummary: DeletionSummary } | 用于跟踪的任务ID( 如果全部被删除的话) 和第一批摘要 |
最佳服务: 大删除树( 大小)
一致性 : 每批原子, 间批最终。 每批都是单独的Convex交易.
进展跟踪: 传回 jobId 页:1 useDeletionJobStatus 反应钩
validateRules(ctx)
通过测试查询对每个索引进行测试,验证所有配置的索引是否存在。 应用程序初始化时或仅通过 dev 检查时, 应该呼叫一次 .
await cd.validateRules(ctx);
// Throws descriptive error if any index is missing or misconfigured
| 参数 | 类型 | 说明 |
|---|---|---|
ctx | QueryCtx | Convex 查询或变异上下文 |
patchDb(db)
返回直接投放的代理数据库写入器 .delete() ,强制所有删除都通过 deleteWithCascade作为关键突变中的安全警卫很有用.
export const safeDeleteUser = mutation({
handler: async (ctx, args) => {
const safeDb = cd.patchDb(ctx.db);
// safeDb.delete(id) --> throws "Direct db.delete() is disabled"
// safeDb.query(...) --> works normally
// safeDb.insert(...) --> works normally
// safeDb.patch(...) --> works normally
}
});
makeBatchDeleteHandler(internalMutationBuilder, componentRef)
工厂功能为处理删除批次而创建app-side内部突变. 此函数必须从您的凸起码导出, 这样组件的调度器就可以通过函数控件引用它 .
import { makeBatchDeleteHandler } from "@00akshatsinha00/convex-cascading-delete";
import { components } from "./_generated/api";
import { internalMutation } from "./_generated/server";
export const _cascadeBatchHandler = makeBatchDeleteHandler(
internalMutation,
components.convexCascadingDelete
);
| 参数 | 类型 | 说明 |
|---|---|---|
internalMutationBuilder | InternalMutation | 你的应用软件 internalMutation 来自 _generated/server |
componentRef | ComponentApi | 构成部分参考来源 components.convexCascadingDelete |
| 回返 | FunctionReference<"mutation"> | 内部变异以通过 batchHandlerRef |
如何运作: 返回的突变收到一批 { table, id } 目标,通过 ctx.db.delete(id),然后通过 reportBatchComplete。该组件的调度器调用此函数处理每个批次.
反应钩
useDeletionJobStatus(api, jobId)
监视带有反应性更新的批量删除进度 。 环绕组件的 getJobStatus 查询.
import { useDeletionJobStatus } from "@00akshatsinha00/convex-cascading-delete/react";
import { api } from "../convex/_generated/api";
function DeletionProgress({ jobId }: { jobId: string | null }) {
const status = useDeletionJobStatus(api, jobId);
if (!status) return null;
const progress = (status.completedCount / status.totalTargetCount) * 100;
return (
<div>
<progress value={progress} max={100} />
<p>{status.status}: {status.completedCount} / {status.totalTargetCount}</p>
{status.status === "completed" && (
<pre>{JSON.stringify(JSON.parse(status.completedSummary), null, 2)}</pre>
)}
</div>
);
}
| 参数 | 类型 | 说明 |
|---|---|---|
api | any | 你的应用软件 api 对象从 _generated/api |
jobId | string | null | 工作编号从 deleteWithCascadeBatched,或无法跳过 |
| 回返 | BatchJobStatus | null | 目前的工作状况,如果没有工作/找不到工作,则无效 |
BatchJobStatus 形状 :
| 外地 | 类型 | 说明 |
|---|---|---|
status | "pending" | "processing" | "completed" | "failed" | 当前工作状态 |
totalTargetCount | number | 要删除的文件共计 |
completedCount | number | 迄今删除的文件 |
completedSummary | string | JSON 字符串映射表名以删除计数 |
error | string | undefined | 如果任务失败, 错误消息 |
导出类型
所有类型均可从主要入口导入:
import type {
CascadeRule, // { to: string; via: string; field: string }
CascadeConfig, // { [sourceTable: string]: CascadeRule[] }
DeletionSummary, // { [tableName: string]: number }
DeletionTarget, // { table: string; id: string }
BatchJobStatus, // { status, totalTargetCount, completedCount, completedSummary, error? }
} from "@00akshatsinha00/convex-cascading-delete";
方案要求
你的计划一定有与你的级联规则相匹配的索引 每个规则 via 必须对应 to 表格,还有 field 必须是该索引的第一个字段.
// 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(),
}),
posts: defineTable({
authorId: v.id("users"),
title: v.string(),
content: v.string(),
}).index("byAuthorId", ["authorId"]), // Required for cascade from users
comments: defineTable({
authorId: v.id("users"),
postId: v.id("posts"),
text: v.string(),
})
.index("byAuthorId", ["authorId"]) // For user → comments cascade
.index("byPostId", ["postId"]), // For post → comments cascade
});
相应的级联规则是:
const rules = defineCascadeRules({
users: [
{ to: "posts", via: "byAuthorId", field: "authorId" },
{ to: "comments", via: "byAuthorId", field: "authorId" }
],
posts: [
{ to: "comments", via: "byPostId", field: "postId" }
]
});
实例
多层次等级
const rules = defineCascadeRules({
organizations: [
{ to: "teams", via: "byOrganizationId", field: "organizationId" }
],
teams: [
{ to: "members", via: "byTeamId", field: "teamId" },
{ to: "projects", via: "byTeamId", field: "teamId" }
],
projects: [
{ to: "tasks", via: "byProjectId", field: "projectId" }
],
tasks: [
{ to: "comments", via: "byTaskId", field: "taskId" }
]
});
// Deleting an organization cascades through 5 levels
const summary = await cd.deleteWithCascade(ctx, "organizations", orgId);
// Returns: { organizations: 1, teams: 5, members: 23, projects: 12, tasks: 67, comments: 234 }
分枝带
单亲表可连入多个依赖表:
const rules = defineCascadeRules({
users: [
{ to: "posts", via: "byAuthorId", field: "authorId" },
{ to: "comments", via: "byAuthorId", field: "authorId" },
{ to: "likes", via: "byUserId", field: "userId" },
{ to: "follows", via: "byFollowerId", field: "followerId" }
]
});
循环依赖
该组件通过访问集自动处理循环引用。 无无限循环 :
const rules = defineCascadeRules({
users: [
{ to: "friendships", via: "byUserId", field: "userId" }
],
friendships: [
{ to: "users", via: "byFriendId", field: "friendId" }
]
});
// Safe - visited set prevents re-processing already-seen documents
const summary = await cd.deleteWithCascade(ctx, "users", userId);
使用补丁Db作为安全警卫
import { mutation } from "./_generated/server";
import { cd } from "./cascading";
export const processUser = mutation({
handler: async (ctx, args) => {
// Replace ctx.db with a guarded version for this mutation
const safeCtx = { ...ctx, db: cd.patchDb(ctx.db) };
// All reads work normally
const user = await safeCtx.db.get(args.userId);
// Direct deletes are blocked - forces cascade usage
// safeCtx.db.delete(args.userId) --> throws Error
// Must use cascade delete instead
await cd.deleteWithCascade(ctx, "users", args.userId);
}
});
最佳做法
- 从内线模式开始 - 使用( I)
deleteWithCascade对于大多数案例来说,它更简单,完全原子化 - 切换为大树批次 - 使用( I)
deleteWithCascadeBatched当删除超过4,000个文档以避免交易限制时 - 验证启动规则 - 打电话
validateRules()在一个 dev- 仅初始化函数中及早捕获错误配置的索引 - 在关键突变中使用补丁Db - 防止意外直接删除留下孤儿记录
- 监测批量进度 - 用那个
useDeletionJobStatus显示用户实时删除反馈的钩头 - 测试级联规则 - 利用测试辅助人员核查生产前的预期关系
结构概览
┌─────────────────────────────────────────────────────────────────┐
│ YOUR APP │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Your Mutation │ │
│ │ │ │
│ │ const cd = new CascadingDelete( │ │
│ │ components.convexCascadingDel,│ │
│ │ { rules: cascadeRules } │ │
│ │ ); │ │
│ │ │ ctx.db (YOUR tables) │
│ │ // Inline mode: │─────► .query(table) │
│ │ cd.deleteWithCascade(ctx, │ .withIndex(idx,...) │
│ │ "teams", teamId) │ .collect() │
│ │ │ .delete(id) │
│ │ // Batched mode: │ │
│ │ cd.deleteWithCascadeBatched(ctx,│ │
│ │ "teams", teamId, opts) │ │
│ │ │ │
│ └──────────┬───────────────────────┘ │
│ │ │
│ │ ctx.runMutation(component.lib.createBatchJob, ...)│
│ │ ctx.runQuery(component.lib.getJobStatus, ...) │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ COMPONENT (Isolated — own DB, own transactions) │ │
│ │ │ │
│ │ Table: deletionJobs │ │
│ │ { status, targets, deleteHandle, batchSize, summary } │ │
│ │ │ │
│ │ Functions: │ │
│ │ createBatchJob(targets, handle, batchSize) │ │
│ │ processNextBatch(jobId) │ │
│ │ ├─ scheduler.runAfter(0, deleteHandle, batch) ──┼──►│
│ │ └─ scheduler.runAfter(200ms, self, jobId) │ │
│ │ getJobStatus(jobId) → reactive query │ │
│ │ reportBatchComplete(jobId, summary) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ │ Function handle callback │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Your Batch Delete Handler (via makeBatchDeleteHandler) │ │
│ │ │ │
│ │ handler: async (ctx, { targets, jobId }) => { │ │
│ │ for (t of targets) await ctx.db.delete(t.id); │ │
│ │ await ctx.runMutation(component.reportBatchComplete, │ │
│ │ { jobId, summary }); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
主要建筑制约: Convex 组件无法访问您的应用程序的表格 。 在您的应用程序的突变上下文中, 所有文档的翻转和删除都会运行 ctx.db该部分只管理自己孤立的数据库中的批量工作状态(创建、进步、完成).
一致性保障
内置模式( I)deleteWithCascade(中文(简体) )
- 完全原子 - 所有删除在单一Convex交易中成功或全部失败
- ACID 达标 - 汇兑公司内部的交易担保
- 即时 - 返回完成
DeletionSummary同步
挂载模式( E)deleteWithCascadeBatched(中文(简体) )
- 每批原子 - 每批都是单独的Convex交易
- 最终间批次 - Batches在两百米之间同步处理
- 第一批内入 - 在调用突变中删除最初批次,以便立即反馈
- 其余一批已排定 - 通过组件调度器使用函数手柄进行处理
- 可观察到的进展 - 使用( I)
useDeletionJobStatus钩子或getJobStatus实时状态查询
性能特征
| 特征 | 细节 |
|---|---|
| 内置模式限制 | ~4,000个文档(根据Convex每笔交易16K的写入限制) |
| 批量大小 | 可配置, 默认为每批2,000个文件 |
| 逆变算法 | 深度第一,顺序后(在父母面前删除儿童) |
| 循环检测 | O(1) 通过 Set<string> |
| 索引使用量 | 高效 .withIndex() 查询——无表格扫描 |
| 批次调度延迟 | 分批200米,防止排程器被淹 |
| 遵守对流限制 | 16K写作,32K文档扫描,4,096索引读取,每笔交易1s执行 |
测试
软件包导出一个测试助手, 用于 convex-test数字 :
import { convexTest } from "convex-test";
import { register } from "@00akshatsinha00/convex-cascading-delete/test";
import schema from "./schema";
const modules = import.meta.glob("./convex/**/*.ts");
test("cascading delete works", async () => {
const t = convexTest(schema, modules);
register(t, "convexCascadingDelete");
// ... your test code using the component
});
这个 register 函数在测试实例中记录组件的策略和模块。 第二个参数必须与您中的组件名称匹配 convex.config.ts。 。 。 .
运行示例
这个 example/ 目录包含一个完整的工作应用程序,显示内线和分批删除模式,并具有5级组织层级.
# Clone the repository
git clone https://github.com/akshatsinha0/convex-cascading-delete.git
cd convex-cascading-delete
# Install dependencies
npm install
# Start the dev server (backend + frontend + build watcher)
npm run dev
例如
- 种子数据按钮 - 创建有团队、成员、项目、任务和评论的抽样组织
- 内行删除 - 在一次交易中以解剖方式删除一个组织
- 挂起删除 - 删除一个组织在多个分批交易中实时取得进展
- 文档计数器 - 参见所有6个表格中的反应性更新数
- (原始内容存档于2013-10-12) (英语). REST API - HTTP端点在
/api/deletion-job-status?jobId=...外部职务监测
解决问题
“ Index 不存在” 错误
运行 validateRules() 以识别缺失的索引 :
await cd.validateRules(ctx);
// Error: Cascade validation failed: Index "byAuthorId" with field "authorId"
// does not exist on table "posts". Define it in your schema.
// Source table: "users"
将缺少的索引添加到您的计划中 .index("indexName", ["fieldName"])。 。 。 .
批次删除已卡住
直接检查工作状态 :
const status = await ctx.runQuery(
components.convexCascadingDelete.lib.getJobStatus,
{ jobId }
);
console.log(status);
// { status: "processing", totalTargetCount: 500, completedCount: 200, ... }
如果一个工作卡在 "processing" 状态,可能是由于批量处理器功能没有被正确导出或部署不匹配.
交易限额超过
如果内置模式出现交易限制出错而失败, 请切换到分批模式 :
// Instead of:
await cd.deleteWithCascade(ctx, "organizations", orgId);
// Use:
await cd.deleteWithCascadeBatched(ctx, "organizations", orgId, {
batchHandlerRef: internal.cascading._cascadeBatchHandler,
batchSize: 1000 // Reduce batch size if needed
});
带有表格名称的错误类型
动态表格访问使用类型断言 :
const summary = await cd.deleteWithCascade(ctx, "users", userId as any);
现场直播
尝试交互式演示 : https://convex-cascadding-delete.vercel.app 互联网档案馆的存檔,存档日期2013-12-21
找到窃听器了吗? 特质请求?
放在这。 。 。 .
许可证
阿帕奇 - 2.0
已建为
Convex 组件写入挑战 - 全速投放功能