DataFn
Plugins

Soft Delete

Convert hard deletes to soft deletes with flags.

The soft delete plugin converts delete mutations into merge operations that set a deletion flag and timestamp. It also auto-filters soft-deleted records from queries on configured resources.

Setup

import { createDatafnClient, createSoftDeletePlugin } from "@datafn/client";

const client = createDatafnClient({
  schema,
  plugins: [
    createSoftDeletePlugin({
      resources: ["todos", "comments"],
    }),
  ],
});

Configuration

interface SoftDeleteConfig {
  resources: string[];              // Resources that use soft-delete
  deletedAtField?: string;          // Field for deletion timestamp (default: "deletedAt")
  isDeletedField?: string;          // Field for deletion flag (default: "isDeleted")
  getTimestamp?: () => Date;        // Custom timestamp factory (default: () => new Date())
}

Custom Field Names

createSoftDeletePlugin({
  resources: ["todos"],
  deletedAtField: "removed_at",
  isDeletedField: "is_removed",
});

Deterministic Timestamps for Testing

let now = new Date("2025-01-01T00:00:00Z");

createSoftDeletePlugin({
  resources: ["todos"],
  getTimestamp: () => now,
});

Mutation Transformation

When a delete mutation targets a configured resource, the beforeMutation hook converts it to a merge operation:

Input:

{
  resource: "todos",
  operation: "delete",
  id: "t1",
}

Transformed to:

{
  resource: "todos",
  operation: "merge",
  id: "t1",
  record: {
    isDeleted: true,
    deletedAt: new Date(),  // Current timestamp
  },
}

Delete operations on resources not listed in config.resources pass through unchanged.

Query Filtering

The beforeQuery hook automatically adds a filter to exclude soft-deleted records:

Input:

{
  resource: "todos",
  filters: { status: "active" },
}

Transformed to:

{
  resource: "todos",
  filters: {
    $and: [
      { status: "active" },
      { isDeleted: { $ne: true } },
    ],
  },
}

If the query has no existing filters, the soft-delete filter is applied directly:

{
  resource: "todos",
  filters: { isDeleted: { $ne: true } },
}

Querying Deleted Records

To include soft-deleted records in a query, set metadata.includeDeleted: true:

const allTodos = await client.table("todos").query({
  metadata: { includeDeleted: true },
});

Batch Operations

The plugin handles both single mutations and batch mutations. When an array of mutations is passed to beforeMutation, each mutation is individually transformed:

// Both deletes are converted to soft deletes
await client.table("todos").mutate([
  { operation: "delete", id: "t1" },
  { operation: "delete", id: "t2" },
]);

Restoring Soft-Deleted Records

Use the restore utility to clear the deletion flags:

import { restore } from "@datafn/client";

await restore(client.table("todos"), "t1");

This performs a merge mutation that sets isDeleted: false and deletedAt: null.

With custom field names:

await restore(client.table("todos"), "t1", {
  isDeletedField: "is_removed",
  deletedAtField: "removed_at",
});

Schema Requirements

Your resource schema should include the soft-delete fields:

{
  name: "todos",
  version: 1,
  fields: [
    { name: "title", type: "string", required: true },
    { name: "isDeleted", type: "boolean", required: false, default: false },
    { name: "deletedAt", type: "date", required: false },
  ],
}