@nestbolt/soft-delete
Decorator
@SoftDeletable() decorator options and the @IncludeDeleted() metadata marker.
The @SoftDeletable() decorator marks a TypeORM entity as soft-deletable. It stores a small piece of metadata that SoftDeleteService reads to decide which column to write when soft-deleting and restoring.
Basic Usage
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { SoftDeletable } from "@nestbolt/soft-delete";
@SoftDeletable()
@Entity("posts")
export class Post {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
title!: string;
@Column({ name: "deleted_at", type: "datetime", nullable: true, default: null })
deletedAt!: Date | null;
}With no options, the entity uses the module-level columnName (default "deleted_at").
Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
columnName | string | Module-level columnName (default "deleted_at") | Override the deleted-at column name for this specific entity. |
Custom Column Name
Set columnName when a specific entity needs a column other than the module default:
@SoftDeletable({ columnName: "removed_at" })
@Entity("articles")
export class Article {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column({ name: "removed_at", type: "datetime", nullable: true, default: null })
removedAt!: Date | null;
}The decorator's columnName must match the actual database column name on the entity (the name option in @Column()). The service uses the database name to resolve the property name from TypeORM metadata, so the entity's TypeScript field can be called whatever you like.
Per-entity overrides take priority over the module-level columnName. Entities without an explicit override fall back to the module default.
How the Column is Resolved
When the service performs an operation on an entity, it resolves the column name like this:
- If
@SoftDeletable({ columnName: "..." })was provided on the entity, that string is used. - Otherwise, the
columnNamefromSoftDeleteModule.forRoot({ columnName })is used. - Otherwise, the literal
"deleted_at"is used.
If the entity is missing the @SoftDeletable() decorator entirely, SoftDeleteService.isSoftDeletable() returns false and the subscriber won't intercept its remove() calls. The service methods (softDelete(), restore(), forceDelete()) will still execute SQL against the resolved column, so make sure your entity has one.
Composing with Other Decorators
@SoftDeletable() is a pure metadata decorator with no runtime side effects, so it composes cleanly with any TypeORM or other Nestbolt decorators:
import { HasMedia } from "@nestbolt/medialibrary";
import { Sluggable } from "@nestbolt/sluggable";
import { Likeable } from "@nestbolt/likeable";
import { SoftDeletable } from "@nestbolt/soft-delete";
@Sluggable({ from: "name" })
@SoftDeletable()
@Likeable({ type: "Product" })
@HasMedia({ modelType: "Product" })
@Entity("products")
export class ProductEntity {
/* ... */
}Order between Nestbolt decorators does not matter.
@IncludeDeleted()
@IncludeDeleted() is a metadata-only marker decorator that can be applied to a class or method. It sets a flag on the target indicating that queries scoped to it should bypass the soft-delete filter:
import { IncludeDeleted } from "@nestbolt/soft-delete";
@IncludeDeleted()
@Controller("admin/posts")
export class AdminPostsController {
/* ... */
}
@Controller("posts")
export class PostsController {
@IncludeDeleted()
@Get("trashed")
listTrashed() {
/* ... */
}
}The decorator only writes metadata under INCLUDE_DELETED_METADATA_KEY -- it doesn't install any global filter or interceptor on its own. It exists so that you (or a custom guard / interceptor / repository wrapper) can check the flag at query time and decide whether to call withTrashed() instead of the default-filtered query:
import { Reflector } from "@nestjs/core";
import { INCLUDE_DELETED_METADATA_KEY } from "@nestbolt/soft-delete";
const include = this.reflector.getAllAndOverride(INCLUDE_DELETED_METADATA_KEY, [
context.getHandler(),
context.getClass(),
]);This package leaves the policy decision -- "do default reads exclude trashed rows?" -- entirely to the application, so @IncludeDeleted() is a hint, not an enforcement.