npm package: @patolord/softdelete
Description: A softdelete component for Convex.
Latest version: 0.1.2

# Convex Soft Delete Component

[![npm version](https://badge.fury.io/js/@patolord%2Fsoftdelete.svg)](https://badge.fury.io/js/@patolord/softdelete)

A Convex component that provides soft-delete functionality for your documents.
Instead of permanently deleting documents, move them to a "trash" where they can
be restored or permanently deleted later.

## Features

- **Soft delete any document** - Move documents to an isolated trash table
  instead of permanent deletion
- **Restore documents** - Recover accidentally deleted data with full document
  preservation
- **Auto-purge** - Schedule automatic permanent deletion after a specified time
  period (built-in cron handles cleanup)
- **Track deletion metadata** - Record who deleted a document and when
- **Query by table** - List and filter deleted documents by their original table
- **Hard delete** - Permanently remove documents from trash when needed
- **Idempotent operations** - All mutations are safe to retry without side
  effects

## Installation

```bash
npm install @patolord/softdelete
```

Create a `convex.config.ts` file in your app's `convex/` folder and install the
component:

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import softdelete from "@patolord/softdelete/convex.config";

const app = defineApp();
app.use(softdelete);

export default app;
```

## Usage

### Basic Setup

```ts
import { SoftDelete } from "@patolord/softdelete";
import { components } from "./_generated/api";

const softDelete = new SoftDelete(components.softdelete);
```

### Soft Delete a Document

```ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const deleteUser = mutation({
  args: { userId: v.id("users") },
  handler: async (ctx, args) => {
    // Get the document
    const user = await ctx.db.get(args.userId);
    if (!user) throw new Error("User not found");

    // Store in soft-delete trash with auto-purge after 30 days
    await softDelete.delete(ctx, "users", args.userId, user, {
      deletedBy: "admin",
      purgeAfterMs: 30 * 24 * 60 * 60 * 1000, // Auto-purge after 30 days
    });

    // Remove from original table
    await ctx.db.delete(args.userId);
  },
});
```

**That's it!** The component includes a built-in cron job that automatically
purges expired documents every 15 minutes. No additional setup required.

### Restore a Document

```ts
export const restoreUser = mutation({
  args: { originalUserId: v.string() },
  handler: async (ctx, args) => {
    // Restore from trash
    const userData = await softDelete.restoreByOriginalId(
      ctx,
      "users",
      args.originalUserId,
    );

    if (!userData) throw new Error("Deleted user not found");

    // Re-insert into original table
    const { _id, _creationTime, ...data } = userData;
    return await ctx.db.insert("users", data);
  },
});
```

### List Deleted Documents

```ts
export const listDeletedUsers = query({
  args: {},
  handler: async (ctx) => {
    return await softDelete.listByTable(ctx, "users");
  },
});
```

### Permanently Delete

```ts
export const permanentlyDelete = mutation({
  args: { deletedDocId: v.string() },
  handler: async (ctx, args) => {
    return await softDelete.hardDelete(ctx, args.deletedDocId);
  },
});
```

## Auto-Purge Behavior

When you set `purgeAfterMs` on a document, the component automatically handles
cleanup:

```ts
await softDelete.delete(ctx, "todos", id, todo, {
  purgeAfterMs: 7 * 24 * 60 * 60 * 1000, // 7 days
});
// Document will be permanently deleted ~7 days after soft-deletion
```

### How It Works

1. `purgeAfterMs` sets a `scheduledPurgeAt` timestamp on the document
2. A built-in cron job runs every **15 minutes**
3. The cron finds all documents past their `scheduledPurgeAt` and deletes them

### Custom Purge Frequency

The default 15-minute interval works for most use cases. If you need faster
cleanup (e.g., documents that expire in seconds), add your own cron:

```ts
// convex/crons.ts
import { cronJobs } from "convex/server";
import { components } from "./_generated/api";

const crons = cronJobs();

// Fast purge for short-lived documents (every 10 seconds)
crons.interval(
  "fast purge",
  { seconds: 10 },
  components.softdelete.lib.purgeExpired,
  { limit: 100 },
);

export default crons;
```

You can run multiple crons at different intervals - they all call the same
`purgeExpired` function which finds and deletes expired documents.

## API Reference

### `SoftDelete` Class

#### Constructor

```ts
const softDelete = new SoftDelete(components.softdelete);
```

#### Methods

| Method                                               | Description                   |
| ---------------------------------------------------- | ----------------------------- |
| `delete(ctx, table, originalId, document, options?)` | Soft-delete a document        |
| `restore(ctx, deletedDocId)`                         | Restore by deleted doc ID     |
| `restoreByOriginalId(ctx, table, originalId)`        | Restore by original ID        |
| `hardDelete(ctx, deletedDocId)`                      | Permanently delete            |
| `hardDeleteByTable(ctx, table, limit?)`              | Empty trash for a table       |
| `get(ctx, deletedDocId)`                             | Get a deleted document        |
| `getByOriginalId(ctx, table, originalId)`            | Find by original ID           |
| `listByTable(ctx, table, limit?)`                    | List deleted docs for a table |
| `listAll(ctx, limit?)`                               | List all deleted docs         |
| `purgeExpired(ctx, limit?)`                          | Manually purge expired docs   |

### Options for `delete()`

| Option         | Type     | Description                             |
| -------------- | -------- | --------------------------------------- |
| `deletedBy`    | `string` | User ID or identifier of who deleted    |
| `purgeAfterMs` | `number` | Auto-purge after this many milliseconds |

### Deleted Document Structure

```ts
interface DeletedDocument {
  _id: string;
  _creationTime: number;
  table: string; // Original table name
  originalId: string; // Original document ID
  document: object; // The full document data
  deletedAt: number; // Timestamp of deletion
  deletedBy?: string; // Who deleted it
  scheduledPurgeAt?: number; // When to auto-purge
}
```

## Counting Deleted Documents

This component intentionally does not include a count function. For efficient
counting at scale, use the
[Aggregate component](https://www.convex.dev/components/aggregate) alongside
this one, or derive counts from `listByTable`:

```ts
const deleted = await softDelete.listByTable(ctx, "users");
const count = deleted.length;
```

## Development

```bash
npm install
npm run dev
```

## License

Apache-2.0