{"_id":"@00akshatsinha00/convex-cascading-delete","name":"@00akshatsinha00/convex-cascading-delete","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@00akshatsinha00/convex-cascading-delete","description":"A Convex component for managing cascading deletes across related documents with atomic and batched deletion modes.","repository":{"type":"git","url":"git+https://github.com/akshatsinha0/convex-cascading-delete.git"},"homepage":"https://github.com/akshatsinha0/convex-cascading-delete#readme","bugs":{"url":"https://github.com/akshatsinha0/convex-cascading-delete/issues"},"version":"0.1.0","license":"Apache-2.0","keywords":["convex","component"],"type":"module","scripts":{"dev":"run-p -r dev:backend dev:frontend dev:build","dev:backend":"convex dev --typecheck-components","dev:frontend":"cd example && vite --clearScreen false","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial","predev":"path-exists .env.local dist || (npm run build && convex dev --once)","build":"tsc --project ./tsconfig.build.json","build:example":"cd example && vite build","build:codegen":"npx convex codegen --component-dir ./src/component && npm run build","build:clean":"rm -rf dist *.tsbuildinfo && npm run build:codegen","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","lint":"eslint .","all":"run-p -r dev:backend dev:frontend dev:build test:watch","test":"vitest run --typecheck","test:watch":"vitest --typecheck --clearScreen false","test:debug":"vitest --inspect-brk --no-file-parallelism","test:coverage":"vitest run --coverage --coverage.reporter=text","preversion":"npm ci && npm run build:clean && run-p test lint typecheck","prepublishOnly":"npm whoami || npm login","alpha":"npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags","release":"npm version patch && npm publish && git push --follow-tags","version":"vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"},"exports":{"./package.json":"./package.json",".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./test":"./src/test.ts","./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"},"./_generated/component":{"types":"./dist/component/_generated/component.d.ts"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"}},"peerDependencies":{"convex":"^1.31.7","react":"^18.3.1 || ^19.0.0"},"devDependencies":{"@convex-dev/eslint-plugin":"^1.1.1","@edge-runtime/vm":"^5.0.0","@eslint/eslintrc":"^3.3.3","@eslint/js":"9.39.2","@types/node":"^24.10.11","@types/react":"^19.2.13","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^5.1.3","chokidar-cli":"3.0.0","convex":"1.31.7","convex-test":"0.0.41","cpy-cli":"^6.0.0","eslint":"9.39.2","eslint-plugin-react":"^7.37.5","eslint-plugin-react-hooks":"^7.0.1","eslint-plugin-react-refresh":"^0.5.0","globals":"^17.3.0","npm-run-all2":"8.0.4","path-exists-cli":"2.0.0","pkg-pr-new":"^0.0.63","prettier":"3.8.1","react":"^19.2.4","react-dom":"^19.2.4","typescript":"5.9.3","typescript-eslint":"8.54.0","vite":"7.3.1","vitest":"4.0.18"},"types":"./dist/client/index.d.ts","module":"./dist/client/index.js","gitHead":"c5a34135d55f87ff520233432129c9b22543cbff","_id":"@00akshatsinha00/convex-cascading-delete@0.1.0","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-xtNav8fwYFXkX+v8wdtfcuH++Wkrsk3xLBb+NTpFUON2mElOE2mpZQPgXDySe2I7JKDOUpcJZRvwQ0mKvQlqcQ==","shasum":"d86bd1759d2e7cadb4eb2810e0355dc5d8f26c78","tarball":"https://registry.npmjs.org/@00akshatsinha00/convex-cascading-delete/-/convex-cascading-delete-0.1.0.tgz","fileCount":67,"unpackedSize":180260,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFFsAeCGo53RhwtWskh3Wm7CLg2S3zsFZE1PiICRNOPDAiEAm9Q2cT5+Ha41x9I9IEXi1MHkZsPzSRcJrJDZ6GJks4A="}]},"_npmUser":{"name":"00akshatsinha00","email":"akshatsinhasramhardy@gmail.com"},"directories":{},"maintainers":[{"name":"00akshatsinha00","email":"akshatsinhasramhardy@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convex-cascading-delete_0.1.0_1770645675280_0.39093410347987056"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-09T14:01:15.184Z","0.1.0":"2026-02-09T14:01:15.437Z","modified":"2026-02-09T14:01:15.680Z"},"maintainers":[{"name":"00akshatsinha00","email":"akshatsinhasramhardy@gmail.com"}],"description":"A Convex component for managing cascading deletes across related documents with atomic and batched deletion modes.","homepage":"https://github.com/akshatsinha0/convex-cascading-delete#readme","keywords":["convex","component"],"repository":{"type":"git","url":"git+https://github.com/akshatsinha0/convex-cascading-delete.git"},"bugs":{"url":"https://github.com/akshatsinha0/convex-cascading-delete/issues"},"license":"Apache-2.0","readme":"# Convex Cascading Delete\n\n[![npm version](https://badge.fury.io/js/@00akshatsinha00%2Fconvex-cascading-delete.svg)](https://www.npmjs.com/package/@00akshatsinha00/convex-cascading-delete)\n\nA Convex component for managing cascading deletes across related documents. Configure relationships via existing indexes, then delete documents safely knowing all related records will be cleaned up automatically with clear consistency guarantees.\n\n## Why Use This Component?\n\n- **Works with existing schemas** - No migration to special schema definitions; uses your existing `defineTable` and indexes\n- **Explicit configuration** - Clear, declarative rules for cascade relationships defined in one place\n- **Two deletion modes** - Inline (atomic, single transaction) for small deletes, batched (scheduled) for large trees\n- **Progress tracking** - React hook for real-time batch deletion progress with reactive updates\n- **Safety guards** - Optional `patchDb` helper prevents accidental direct `db.delete` calls\n- **Index validation** - Catch configuration errors at startup, not at delete time\n- **Circular handling** - Automatically handles circular and diamond dependencies via visited set\n- **Full observability** - Returns deletion summary with per-table document counts\n- **Non-invasive** - Drop-in component that doesn't replace your schema builder or require code changes beyond deletion calls\n\n## Pre-requisite: Convex\n\nYou'll need an existing Convex project to use this component. Convex is a hosted backend platform, including a database, serverless functions, and a bunch more you can learn about [here](https://docs.convex.dev/get-started).\n\nRun `npm create convex` or follow any of the [Convex quickstarts](https://docs.convex.dev/home) to set one up.\n\n## Installation\n\n### Step 1: Install the package\n\n```bash\nnpm install @00akshatsinha00/convex-cascading-delete\n```\n\n### Step 2: Add the component to your Convex app\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport convexCascadingDelete from \"@00akshatsinha00/convex-cascading-delete/convex.config\";\n\nconst app = defineApp();\napp.use(convexCascadingDelete);\n\nexport default app;\n```\n\n### Step 3: Configure cascade rules and instantiate\n\n```ts\n// convex/cascading.ts\nimport {\n  CascadingDelete,\n  defineCascadeRules,\n  makeBatchDeleteHandler\n} from \"@00akshatsinha00/convex-cascading-delete\";\nimport { components } from \"./_generated/api\";\nimport { internalMutation } from \"./_generated/server\";\n\nexport const cascadeRules = defineCascadeRules({\n  users: [\n    { to: \"posts\", via: \"byAuthorId\", field: \"authorId\" },\n    { to: \"comments\", via: \"byAuthorId\", field: \"authorId\" }\n  ],\n  posts: [\n    { to: \"comments\", via: \"byPostId\", field: \"postId\" }\n  ]\n});\n\nexport const cd = new CascadingDelete(components.convexCascadingDelete, {\n  rules: cascadeRules\n});\n\n// Required for batched mode - exports an internal mutation that processes deletion batches\nexport const _cascadeBatchHandler = makeBatchDeleteHandler(\n  internalMutation,\n  components.convexCascadingDelete\n);\n```\n\n## Quick Start\n\nUse the configured `cd` instance in your mutations:\n\n```ts\n// convex/users.ts\nimport { mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { cd } from \"./cascading\";\n\nexport const deleteUser = mutation({\n  args: { userId: v.id(\"users\") },\n  handler: async (ctx, { userId }) => {\n    // Deletes user + all their posts + all comments on those posts\n    const summary = await cd.deleteWithCascade(ctx, \"users\", userId);\n    console.log(\"Deleted:\", summary);\n    // Returns: { users: 1, posts: 5, comments: 23 }\n  }\n});\n```\n\nFor large deletion trees, use batched mode:\n\n```ts\n// convex/organizations.ts\nimport { mutation } from \"./_generated/server\";\nimport { internal } from \"./_generated/api\";\nimport { v } from \"convex/values\";\nimport { cd } from \"./cascading\";\n\nexport const deleteOrganization = mutation({\n  args: { orgId: v.id(\"organizations\") },\n  handler: async (ctx, { orgId }) => {\n    const result = await cd.deleteWithCascadeBatched(\n      ctx,\n      \"organizations\",\n      orgId,\n      {\n        batchHandlerRef: internal.cascading._cascadeBatchHandler,\n        batchSize: 2000\n      }\n    );\n    // result.jobId can be used to track progress via useDeletionJobStatus hook\n    // result.initialSummary contains counts from the first inline batch\n    return result;\n  }\n});\n```\n\n## API Reference\n\n### `defineCascadeRules(config)`\n\nDefines and validates cascade relationships between tables. Returns a frozen configuration object.\n\n```ts\nconst rules = defineCascadeRules({\n  [sourceTable: string]: [\n    {\n      to: string,       // Target table name to cascade to\n      via: string,       // Index name on target table\n      field: string      // Field in index used for equality matching (holds parent ID)\n    }\n  ]\n});\n```\n\n**Requirements:**\n- The index specified by `via` must exist on the target table\n- The index must include the field specified by `field`\n- The `field` must contain IDs from the source table\n\n**Validation performed:**\n- All properties (`to`, `via`, `field`) must be present and be strings\n- Duplicate rules (same `to:via:field` combination) are rejected\n- Configuration must be a non-null object\n\n### `CascadingDelete` Class\n\nMain interface for deletion operations.\n\n#### Constructor\n\n```ts\nconst cd = new CascadingDelete(components.convexCascadingDelete, { rules });\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `component` | `ComponentApi` | Component reference from `components.convexCascadingDelete` |\n| `options.rules` | `CascadeConfig` | Rules from `defineCascadeRules()` |\n\n#### `deleteWithCascade(ctx, table, id)`\n\nDeletes a document and all its cascading dependents in a single transaction. Uses depth-first post-order traversal (children deleted before parents) with a visited set for cycle detection.\n\n```ts\nconst summary: DeletionSummary = await cd.deleteWithCascade(ctx, \"users\", userId);\n// Returns: { users: 1, posts: 5, comments: 23 }\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `ctx` | `MutationCtx` | Convex mutation context |\n| `table` | `string` | Source table name |\n| `id` | `string` | Document ID to delete |\n| **Returns** | `DeletionSummary` | Map of table names to deleted document counts |\n\n**Best for:** Small to medium deletion trees (fewer than 4,000 documents)\n\n**Consistency:** Fully atomic - all deletes succeed or all fail within a single Convex transaction\n\n#### `deleteWithCascadeBatched(ctx, table, id, options)`\n\nDeletes a document and its dependents across multiple batched transactions. Collects all targets first via read-only traversal, deletes the first batch inline, then schedules remaining batches via the component's job system.\n\n```ts\nconst result = await cd.deleteWithCascadeBatched(\n  ctx,\n  \"organizations\",\n  orgId,\n  {\n    batchHandlerRef: internal.cascading._cascadeBatchHandler,\n    batchSize: 2000  // Optional, defaults to 2000\n  }\n);\n// Returns: { jobId: \"j57a...\", initialSummary: { organizations: 1, teams: 3 } }\n// jobId is null if all targets fit in the first batch\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `ctx` | `MutationCtx` | Convex mutation context |\n| `table` | `string` | Source table name |\n| `id` | `string` | Document ID to delete |\n| `options.batchHandlerRef` | `FunctionReference<\"mutation\">` | Reference to your exported batch handler (from `makeBatchDeleteHandler`) |\n| `options.batchSize` | `number` (optional) | Documents per batch, defaults to 2000 |\n| **Returns** | `{ jobId: string \\| null, initialSummary: DeletionSummary }` | Job ID for tracking (null if all deleted inline) and first-batch summary |\n\n**Best for:** Large deletion trees (any size)\n\n**Consistency:** Per-batch atomic, inter-batch eventual. Each batch is a separate Convex transaction.\n\n**Progress tracking:** Pass the returned `jobId` to the `useDeletionJobStatus` React hook\n\n#### `validateRules(ctx)`\n\nValidates that all configured indexes exist by probing each index with a test query. Should be called once during app initialization or in a dev-only check.\n\n```ts\nawait cd.validateRules(ctx);\n// Throws descriptive error if any index is missing or misconfigured\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `ctx` | `QueryCtx` | Convex query or mutation context |\n\n#### `patchDb(db)`\n\nReturns a proxied database writer that throws on direct `.delete()` calls, forcing all deletions to go through `deleteWithCascade`. Useful as a safety guard in critical mutations.\n\n```ts\nexport const safeDeleteUser = mutation({\n  handler: async (ctx, args) => {\n    const safeDb = cd.patchDb(ctx.db);\n    // safeDb.delete(id)  --> throws \"Direct db.delete() is disabled\"\n    // safeDb.query(...)   --> works normally\n    // safeDb.insert(...)  --> works normally\n    // safeDb.patch(...)   --> works normally\n  }\n});\n```\n\n### `makeBatchDeleteHandler(internalMutationBuilder, componentRef)`\n\nFactory function that creates the app-side internal mutation for processing deletion batches. This function must be exported from your convex code so the component's scheduler can invoke it via a function handle.\n\n```ts\nimport { makeBatchDeleteHandler } from \"@00akshatsinha00/convex-cascading-delete\";\nimport { components } from \"./_generated/api\";\nimport { internalMutation } from \"./_generated/server\";\n\nexport const _cascadeBatchHandler = makeBatchDeleteHandler(\n  internalMutation,\n  components.convexCascadingDelete\n);\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `internalMutationBuilder` | `InternalMutation` | Your app's `internalMutation` builder from `_generated/server` |\n| `componentRef` | `ComponentApi` | Component reference from `components.convexCascadingDelete` |\n| **Returns** | `FunctionReference<\"mutation\">` | Internal mutation to pass as `batchHandlerRef` |\n\n**How it works:** The returned mutation receives a batch of `{ table, id }` targets, deletes each one via `ctx.db.delete(id)`, then reports completion back to the component via `reportBatchComplete`. The component's scheduler calls this function handle with each batch.\n\n### React Hook\n\n#### `useDeletionJobStatus(api, jobId)`\n\nMonitors batch deletion progress with reactive updates. Wraps the component's `getJobStatus` query.\n\n```tsx\nimport { useDeletionJobStatus } from \"@00akshatsinha00/convex-cascading-delete/react\";\nimport { api } from \"../convex/_generated/api\";\n\nfunction DeletionProgress({ jobId }: { jobId: string | null }) {\n  const status = useDeletionJobStatus(api, jobId);\n\n  if (!status) return null;\n\n  const progress = (status.completedCount / status.totalTargetCount) * 100;\n\n  return (\n    <div>\n      <progress value={progress} max={100} />\n      <p>{status.status}: {status.completedCount} / {status.totalTargetCount}</p>\n      {status.status === \"completed\" && (\n        <pre>{JSON.stringify(JSON.parse(status.completedSummary), null, 2)}</pre>\n      )}\n    </div>\n  );\n}\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `api` | `any` | Your app's `api` object from `_generated/api` |\n| `jobId` | `string \\| null` | Job ID from `deleteWithCascadeBatched`, or null to skip |\n| **Returns** | `BatchJobStatus \\| null` | Current job status, or null if no job / job not found |\n\n**`BatchJobStatus` shape:**\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `status` | `\"pending\" \\| \"processing\" \\| \"completed\" \\| \"failed\"` | Current job state |\n| `totalTargetCount` | `number` | Total documents to delete |\n| `completedCount` | `number` | Documents deleted so far |\n| `completedSummary` | `string` | JSON string mapping table names to deleted counts |\n| `error` | `string \\| undefined` | Error message if job failed |\n\n## Exported Types\n\nAll types are importable from the main entry point:\n\n```ts\nimport type {\n  CascadeRule,       // { to: string; via: string; field: string }\n  CascadeConfig,     // { [sourceTable: string]: CascadeRule[] }\n  DeletionSummary,   // { [tableName: string]: number }\n  DeletionTarget,    // { table: string; id: string }\n  BatchJobStatus,    // { status, totalTargetCount, completedCount, completedSummary, error? }\n} from \"@00akshatsinha00/convex-cascading-delete\";\n```\n\n## Schema Requirements\n\nYour schema must have indexes that match your cascade rules. Each rule's `via` must correspond to an index on the `to` table, and the `field` must be the first field in that index.\n\n```ts\n// convex/schema.ts\nimport { defineSchema, defineTable } from \"convex/server\";\nimport { v } from \"convex/values\";\n\nexport default defineSchema({\n  users: defineTable({\n    name: v.string(),\n    email: v.string(),\n  }),\n\n  posts: defineTable({\n    authorId: v.id(\"users\"),\n    title: v.string(),\n    content: v.string(),\n  }).index(\"byAuthorId\", [\"authorId\"]),  // Required for cascade from users\n\n  comments: defineTable({\n    authorId: v.id(\"users\"),\n    postId: v.id(\"posts\"),\n    text: v.string(),\n  })\n    .index(\"byAuthorId\", [\"authorId\"])   // For user → comments cascade\n    .index(\"byPostId\", [\"postId\"]),      // For post → comments cascade\n});\n```\n\nThe corresponding cascade rules would be:\n\n```ts\nconst rules = defineCascadeRules({\n  users: [\n    { to: \"posts\", via: \"byAuthorId\", field: \"authorId\" },\n    { to: \"comments\", via: \"byAuthorId\", field: \"authorId\" }\n  ],\n  posts: [\n    { to: \"comments\", via: \"byPostId\", field: \"postId\" }\n  ]\n});\n```\n\n## Examples\n\n### Multi-Level Hierarchy\n\n```ts\nconst rules = defineCascadeRules({\n  organizations: [\n    { to: \"teams\", via: \"byOrganizationId\", field: \"organizationId\" }\n  ],\n  teams: [\n    { to: \"members\", via: \"byTeamId\", field: \"teamId\" },\n    { to: \"projects\", via: \"byTeamId\", field: \"teamId\" }\n  ],\n  projects: [\n    { to: \"tasks\", via: \"byProjectId\", field: \"projectId\" }\n  ],\n  tasks: [\n    { to: \"comments\", via: \"byTaskId\", field: \"taskId\" }\n  ]\n});\n\n// Deleting an organization cascades through 5 levels\nconst summary = await cd.deleteWithCascade(ctx, \"organizations\", orgId);\n// Returns: { organizations: 1, teams: 5, members: 23, projects: 12, tasks: 67, comments: 234 }\n```\n\n### Branching Cascades\n\nA single parent table can cascade to multiple dependent tables:\n\n```ts\nconst rules = defineCascadeRules({\n  users: [\n    { to: \"posts\", via: \"byAuthorId\", field: \"authorId\" },\n    { to: \"comments\", via: \"byAuthorId\", field: \"authorId\" },\n    { to: \"likes\", via: \"byUserId\", field: \"userId\" },\n    { to: \"follows\", via: \"byFollowerId\", field: \"followerId\" }\n  ]\n});\n```\n\n### Circular Dependencies\n\nThe component handles circular references automatically via a visited set. No infinite loops:\n\n```ts\nconst rules = defineCascadeRules({\n  users: [\n    { to: \"friendships\", via: \"byUserId\", field: \"userId\" }\n  ],\n  friendships: [\n    { to: \"users\", via: \"byFriendId\", field: \"friendId\" }\n  ]\n});\n\n// Safe - visited set prevents re-processing already-seen documents\nconst summary = await cd.deleteWithCascade(ctx, \"users\", userId);\n```\n\n### Using patchDb as a Safety Guard\n\n```ts\nimport { mutation } from \"./_generated/server\";\nimport { cd } from \"./cascading\";\n\nexport const processUser = mutation({\n  handler: async (ctx, args) => {\n    // Replace ctx.db with a guarded version for this mutation\n    const safeCtx = { ...ctx, db: cd.patchDb(ctx.db) };\n\n    // All reads work normally\n    const user = await safeCtx.db.get(args.userId);\n\n    // Direct deletes are blocked - forces cascade usage\n    // safeCtx.db.delete(args.userId)  --> throws Error\n\n    // Must use cascade delete instead\n    await cd.deleteWithCascade(ctx, \"users\", args.userId);\n  }\n});\n```\n\n## Best Practices\n\n1. **Start with inline mode** - Use `deleteWithCascade` for most cases; it's simpler and fully atomic\n2. **Switch to batched for large trees** - Use `deleteWithCascadeBatched` when deleting more than 4,000 documents to avoid transaction limits\n3. **Validate rules on startup** - Call `validateRules()` in a dev-only initialization function to catch misconfigured indexes early\n4. **Use patchDb in critical mutations** - Prevent accidental direct deletes that would leave orphaned records\n5. **Monitor batch progress** - Use the `useDeletionJobStatus` hook to show users real-time deletion feedback\n6. **Test cascade rules** - Verify relationships work as expected before production using the testing helpers\n\n## Architecture Overview\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│  YOUR APP                                                       │\n│                                                                 │\n│  ┌──────────────────────────────────┐                           │\n│  │  Your Mutation                   │                           │\n│  │                                  │                           │\n│  │  const cd = new CascadingDelete( │                           │\n│  │    components.convexCascadingDel,│                           │\n│  │    { rules: cascadeRules }       │                           │\n│  │  );                              │                           │\n│  │                                  │  ctx.db (YOUR tables)     │\n│  │  // Inline mode:                 │─────► .query(table)       │\n│  │  cd.deleteWithCascade(ctx,       │       .withIndex(idx,...) │\n│  │    \"teams\", teamId)              │       .collect()          │\n│  │                                  │       .delete(id)         │\n│  │  // Batched mode:                │                           │\n│  │  cd.deleteWithCascadeBatched(ctx,│                           │\n│  │    \"teams\", teamId, opts)        │                           │\n│  │                                  │                           │\n│  └──────────┬───────────────────────┘                           │\n│             │                                                   │\n│             │ ctx.runMutation(component.lib.createBatchJob, ...)│\n│             │ ctx.runQuery(component.lib.getJobStatus, ...)     │\n│             ▼                                                   │\n│  ┌──────────────────────────────────────────────────────────┐   │\n│  │  COMPONENT (Isolated — own DB, own transactions)         │   │\n│  │                                                          │   │\n│  │  Table: deletionJobs                                     │   │\n│  │    { status, targets, deleteHandle, batchSize, summary } │   │\n│  │                                                          │   │\n│  │  Functions:                                              │   │\n│  │    createBatchJob(targets, handle, batchSize)            │   │\n│  │    processNextBatch(jobId)                               │   │\n│  │      ├─ scheduler.runAfter(0, deleteHandle, batch)     ──┼──►│\n│  │      └─ scheduler.runAfter(200ms, self, jobId)           │   │\n│  │    getJobStatus(jobId) → reactive query                  │   │\n│  │    reportBatchComplete(jobId, summary)                   │   │\n│  └──────────────────────────────────────────────────────────┘   │\n│             │                                                   │\n│             │ Function handle callback                          │\n│             ▼                                                   │\n│  ┌──────────────────────────────────────────────────────────┐   │\n│  │  Your Batch Delete Handler (via makeBatchDeleteHandler)  │   │\n│  │                                                          │   │\n│  │  handler: async (ctx, { targets, jobId }) => {           │   │\n│  │    for (t of targets) await ctx.db.delete(t.id);         │   │\n│  │    await ctx.runMutation(component.reportBatchComplete,  │   │\n│  │      { jobId, summary });                                │   │\n│  │  }                                                       │   │\n│  └──────────────────────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n**Key architectural constraint:** Convex components cannot access your app's tables. All document traversal and deletion runs in your app's mutation context using `ctx.db`. The component only manages batch job state (creation, progress, completion) in its own isolated database.\n\n## Consistency Guarantees\n\n### Inline Mode (`deleteWithCascade`)\n- **Fully atomic** - All deletes succeed or all fail within a single Convex transaction\n- **ACID compliant** - Leverages Convex's built-in transactional guarantees\n- **Immediate** - Returns complete `DeletionSummary` synchronously\n\n### Batched Mode (`deleteWithCascadeBatched`)\n- **Per-batch atomic** - Each batch is a separate Convex transaction\n- **Inter-batch eventual** - Batches process asynchronously with 200ms delay between them\n- **First batch inline** - Initial batch is deleted in the calling mutation for immediate feedback\n- **Remaining batches scheduled** - Processed via the component's scheduler using function handles\n- **Progress observable** - Use `useDeletionJobStatus` hook or `getJobStatus` query for real-time status\n\n## Performance Characteristics\n\n| Characteristic | Detail |\n|---|---|\n| **Inline mode limit** | ~4,000 documents (based on Convex's 16K write limit per transaction) |\n| **Batch size** | Configurable, defaults to 2,000 documents per batch |\n| **Traversal algorithm** | Depth-first, post-order (children deleted before parents) |\n| **Cycle detection** | O(1) lookup per document via `Set<string>` |\n| **Index usage** | Efficient `.withIndex()` queries — no table scans |\n| **Batch scheduling delay** | 200ms between batches to prevent scheduler flooding |\n| **Convex limits respected** | 16K writes, 32K document scans, 4,096 index reads, 1s execution per transaction |\n\n## Testing\n\nThe package exports a test helper for use with `convex-test`:\n\n```ts\nimport { convexTest } from \"convex-test\";\nimport { register } from \"@00akshatsinha00/convex-cascading-delete/test\";\nimport schema from \"./schema\";\n\nconst modules = import.meta.glob(\"./convex/**/*.ts\");\n\ntest(\"cascading delete works\", async () => {\n  const t = convexTest(schema, modules);\n  register(t, \"convexCascadingDelete\");\n\n  // ... your test code using the component\n});\n```\n\nThe `register` function registers the component's schema and modules with the test instance. The second argument must match the component name in your `convex.config.ts`.\n\n## Running the Example\n\nThe `example/` directory contains a full working application demonstrating both inline and batched deletion modes with a 5-level organizational hierarchy.\n\n```bash\n# Clone the repository\ngit clone https://github.com/akshatsinha0/convex-cascading-delete.git\ncd convex-cascading-delete\n\n# Install dependencies\nnpm install\n\n# Start the dev server (backend + frontend + build watcher)\nnpm run dev\n```\n\nThe example app includes:\n- **Seed data buttons** - Create sample organizations with teams, members, projects, tasks, and comments\n- **Inline delete** - Delete an organization atomically in a single transaction\n- **Batched delete** - Delete an organization across multiple batched transactions with real-time progress\n- **Document counters** - See counts update reactively across all 6 tables\n- **REST API** - HTTP endpoint at `/api/deletion-job-status?jobId=...` for external job monitoring\n\n## Troubleshooting\n\n### \"Index does not exist\" error\n\nRun `validateRules()` to identify missing indexes:\n\n```ts\nawait cd.validateRules(ctx);\n// Error: Cascade validation failed: Index \"byAuthorId\" with field \"authorId\"\n// does not exist on table \"posts\". Define it in your schema.\n// Source table: \"users\"\n```\n\nAdd the missing index to your schema with `.index(\"indexName\", [\"fieldName\"])`.\n\n### Batch deletion stuck\n\nCheck job status directly:\n\n```ts\nconst status = await ctx.runQuery(\n  components.convexCascadingDelete.lib.getJobStatus,\n  { jobId }\n);\nconsole.log(status);\n// { status: \"processing\", totalTargetCount: 500, completedCount: 200, ... }\n```\n\nIf a job is stuck in `\"processing\"` state, it may be due to the batch handler function not being properly exported or a deployment mismatch.\n\n### Transaction limit exceeded\n\nIf inline mode fails with a transaction limit error, switch to batched mode:\n\n```ts\n// Instead of:\nawait cd.deleteWithCascade(ctx, \"organizations\", orgId);\n\n// Use:\nawait cd.deleteWithCascadeBatched(ctx, \"organizations\", orgId, {\n  batchHandlerRef: internal.cascading._cascadeBatchHandler,\n  batchSize: 1000  // Reduce batch size if needed\n});\n```\n\n### Type errors with table names\n\nUse type assertions for dynamic table access:\n\n```ts\nconst summary = await cd.deleteWithCascade(ctx, \"users\", userId as any);\n```\n\n## Live Demo\n\nTry the interactive demo: [https://convex-cascading-delete.vercel.app](https://convex-cascading-delete.vercel.app)\n\n## Found a bug? Feature request?\n\n[File it here](https://github.com/akshatsinha0/convex-cascading-delete/issues).\n\n## License\n\nApache-2.0\n\n## Built For\n\n[Convex Components Authoring Challenge](https://docs.convex.dev/components/authoring) - Full-Stack Drop-In Features\n","readmeFilename":"README.md","_rev":"1-14af5c7c0a15d2bffd51685993b00168"}