# Cascading Deletes

Relationship-based cleanup

- Package: @bijectionhq/cascading-deletes
- Category: Database & Backend
- Status: Reference
- License: Apache-2.0
- Bijection source (repository access required): https://github.com/bijectionhq/cascading-deletes/tree/acb1222095c6f18a7f6e0e949894cbf4717970c9
- Upstream: https://github.com/sholajegede/convex-cascading-deletes/tree/33ebb486a722c08450dc5a8e016af9e60980ed24
- Documentation: https://docs.bijection.com/components/using
- Directory: https://bijection.com/components/cascading-deletes

## Get the source

Bijection repository access is required. Request access at https://bijection.com/contact.

```sh
git clone https://github.com/bijectionhq/cascading-deletes.git
cd cascading-deletes
git checkout acb1222095c6f18a7f6e0e949894cbf4717970c9
```

## Before using this component

Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute.

## Upstream package documentation

Recorded 2026-10-23. This describes @sholajegede/convex-cascading-deletes; check the Bijection adaptation separately.

This component automates cascading deletes across related documents in your Convex database. You configure table relationships once using your existing indexes, then delete a record and all its dependents with a single function call. The component handles batching via the Convex scheduler, prevents infinite loops in circular relationships, and provides queryable deletion logs.

### Use cases

• **Clean up user data on account deletion** - Delete a user and automatically cascade to their posts, comments, likes, and profile data without manually tracking dependencies
• **Remove complex hierarchical data** - Delete a project and cascade through tasks, subtasks, files, and permissions while maintaining referential integrity
• **Enforce deletion policies across teams** - Use the safe db helper to prevent direct deletes and ensure all deletions go through the cascading system
• **Audit and monitor data removal** - Query deletion logs to track what was removed during cascades for compliance or debugging
• **Migrate existing apps to cascading deletes** - Validate your relationship configuration against your current database schema and indexes

### How it works

You instantiate a `CascadingDeletes` client with relationship configurations that map your existing table indexes to parent-child relationships. Each relationship specifies the source table (child), target table (parent), index name, and foreign key field name.

When you call `deleteWithCascade()` in an action, the component traverses the dependency tree and deletes records in the correct order. Large deletion trees are processed in batches via the Convex scheduler to handle transaction limits, while maintaining atomicity within each batch.

The component provides a `getSafeDb()` method that patches `ctx.db` to throw errors on direct `.delete()` calls, enforcing cascade-only deletions throughout your codebase. All cascading operations are logged with per-table deletion counts and can be queried reactively using standard Convex queries.

### Upstream README

# @sholajegede/convex-cascading-deletes

A [Convex component](https://www.convex.dev/components) for managing cascading deletes across related documents. Configure relationships via your existing indexes — when you delete a record, all dependent records are cleaned up automatically.

Found a bug? Feature request? [File it here](https://github.com/sholajegede/convex-cascading-deletes/issues).

## Features

- **Cascading deletes** — delete a record and all its dependents in one call
- **Relationship config** — declare relationships once using your existing indexes
- **Scheduler-based batching** — large deletion trees are processed in batches via the Convex scheduler; deletes within a batch are atomic
- **Circular relationship protection** — visited tracking prevents infinite loops
- **Safe db helper** — patch `ctx.db` to throw on direct `.delete()` calls, enforcing cascade-only deletions
- **Deletion logs** — every cascade is logged with a per-table count, queryable reactively
- **Index validation** — validate your relationship config against the deployment at startup

## Installation
```sh
npm install @sholajegede/convex-cascading-deletes
```

Add the component to your `convex/convex.config.ts`:
```ts
import { defineApp } from "convex/server";
import convexCascadingDeletes from "@sholajegede/convex-cascading-deletes/convex.config.js";

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

export default app;
```

## Usage

Instantiate the client once, declaring your table relationships:
```ts
// convex/cascadeDeletes.ts
import { components } from "./_generated/api.js";
import { CascadingDeletes } from "@sholajegede/convex-cascading-deletes";

export const cascadingDeletes = new CascadingDeletes(components.convexCascadingDeletes, {
  relationships: [
    {
      sourceTable: "posts",      // child table
      targetTable: "users",      // parent table
      indexName: "by_user",      // index on posts that references users
      fieldName: "userId",       // field on posts that holds the user ID
    },
    {
      sourceTable: "comments",
      targetTable: "posts",
      indexName: "by_post",
      fieldName: "postId",
    },
  ],
});
```

### Delete with cascade
```ts
// convex/users.ts
import { action } from "./_generated/server.js";
import { cascadingDeletes } from "./cascadeDeletes.js";
import { v } from "convex/values";

export const deleteUser = action({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const counts = await cascadingDeletes.deleteWithCascade(ctx, {
      table: "users",
      id: args.userId,
    });
    // counts: { users: 1, posts: 4, comments: 12 }
    return counts;
  },
});
```

Deleting a user cascades to their posts, then to each post's comments — all automatically, in the correct order.

### Enforce cascade-only deletions
```ts
export const updatePost = mutation({
  args: { postId: v.id("posts"), title: v.string() },
  handler: async (ctx, args) => {
    const db = cascadingDeletes.getSafeDb(ctx);
    // db.delete() now throws — use deleteWithCascade instead
    await db.patch(args.postId, { title: args.title }); // fine
  },
});
```

### Query deletion logs
```ts
// convex/logs.ts
import { query } from "./_generated/server.js";
import { components } from "./_generated/api.js";
import { v } from "convex/values";

export const getDeletionLog = query({
  args: { table: v.string(), id: v.string() },
  handler: async (ctx, args) => {
    return await ctx.runQuery(components.convexCascadingDeletes.lib.getDeletionLog, {
      rootTable: args.table,
      rootId: args.id,
    });
  },
});
```
```tsx
// React — subscribes reactively
const log = useQuery(api.logs.getDeletionLog, { table: "users", id: userId });
// log.deletedCounts — JSON string: { "users": 1, "posts": 4, "comments": 12 }
// log.deletedAt     — timestamp of when the cascade ran
```

### Validate indexes at startup
```ts
export const onStartup = mutation({
  handler: async (ctx) => {
    await cascadingDeletes.validate(ctx);
  },
});
```

## API

### `CascadingDeletes` class

| Method | Description |
|--------|-------------|
| `deleteWithCascade(ctx, { table, id })` | Delete a record and all configured dependents. Returns `{ [table]: count }`. |
| `getDeletionLog(ctx, { table, id })` | Query the deletion log for a previously deleted record. |
| `getSafeDb(ctx)` | Returns a patched `ctx.db` that throws on `.delete()`. |
| `validate(ctx)` | Validates all configured relationships against the deployment. |

### Relationship config

| Field | Description |
|-------|-------------|
| `sourceTable` | The child table (holds the foreign key) |
| `targetTable` | The parent table (the one being deleted) |
| `indexName` | Index on `sourceTable` that references `targetTable` |
| `fieldName` | Field on `sourceTable` that holds the parent ID |

## Development
```sh
npm i
npm run dev
```

## License

Apache-2.0