Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Soft Delete

A soft-deleted row is left in the database but marked as deleted. DELETE requests flip the marker; list, read, and include queries hide rows whose marker is set. This page covers how a model opts in, the two storage styles, and the query semantics that follow.

Opting in

There are two ways to enable soft delete on a model. Both produce the same behaviour; pick whichever fits the declaration style of the rest of the model.

By embed

Embed one of the framework’s marker types:

type Article struct {
    maniflex.BaseModel
    maniflex.WithDeletedAt              // timestamp-based soft delete
    Title string `json:"title"`
}
EmbedColumn addedStorage style
maniflex.WithDeletedAtdeleted_at — nullable timestamp; NULL means not deletedtimestamp
maniflex.WithIsDeletedis_deleted — boolean; false means not deletedflag

Both columns are tagged readonly and filterable. They are not part of any write request — the framework manages them.

By configuration

The same setup, expressed at registration:

server.MustRegister(
    Article{}, maniflex.ModelConfig{
        SoftDelete: maniflex.SoftDeleteConfig{
            Enabled:   true,
            Field:     "deleted_at",
            FieldType: maniflex.SoftDeleteTimestamp, // or maniflex.SoftDeleteBool
        },
    },
)

If both an embed and a ModelConfig.SoftDelete are present, the explicit config wins.

Choosing between timestamp and boolean

Both styles work; they differ in what you can tell from the column afterwards.

  • WithDeletedAt records when the row was deleted, which makes audit trails, “deleted in the last 30 days” queries, and undelete-with-context possible. It is the default choice.
  • WithIsDeleted stores only the fact of deletion. Use it when the surrounding system already records deletion timestamps elsewhere, or when a boolean fits an existing schema better.

Delete semantics

For a soft-deletable model, DELETE /api/<table>/{id} updates the marker instead of removing the row:

StyleWhat DELETE does
Timestampsets deleted_at to the current UTC time
Booleansets is_deleted to true

The endpoint, the response, and the status code are the same as for a hard-delete model; only the underlying SQL differs.

Query semantics

Once enabled, soft-deleted rows are filtered out everywhere the framework reads the table:

  • List (GET /<table>) — only un-deleted rows are returned.
  • Read (GET /<table>/{id}) — a soft-deleted row returns 404.
  • Includes — relations populated via ?include= skip soft-deleted children.
  • UpdatePATCH on a soft-deleted row returns 404; the row is treated as absent.

To surface the marker for clients that need it (e.g. an admin tool), filter on it explicitly:

# Only soft-deleted rows
curl 'localhost:8080/api/articles?filter=deleted_at:ne:null'

deleted_at and is_deleted are filterable, so the standard filter grammar applies — see Querying.

Restoring a row

The framework does not ship a built-in “undelete” endpoint, because the right semantics differ across applications (does restoring also reset audit fields? republish events?). The mechanics are simple: clear the marker. This is usually done with a custom action that runs a raw UPDATE or calls the adapter directly.

Interaction with hard delete

A model is either soft- or hard-delete; the choice is a property of the model, not the request. If you need a true hard delete on a soft-deletable model — for example, to honour an erasure request — perform it through a raw query or a custom action that bypasses the standard handler.

Quick reference

GoalDeclaration
Timestamp soft deleteembed maniflex.WithDeletedAt
Boolean soft deleteembed maniflex.WithIsDeleted
Soft delete with a custom columnModelConfig.SoftDelete
List only deleted rows?filter=deleted_at:ne:null (timestamp) or ?filter=is_deleted:eq:true (boolean)