Aller au contenu

Soft Delete

Soft delete allows you to mark records as deleted without actually removing them from the database. This is useful for:

  • Audit trails
  • Data recovery
  • Maintaining referential integrity
  • Compliance requirements

When soft delete is enabled:

  • Records are marked as deleted (typically with a deletedAt timestamp)
  • Deleted records are excluded from queries by default
  • You can query deleted records using the withDeleted parameter
  • The deleteOne and deleteMany mutations mark records as deleted instead of removing them

Enable soft delete in your resolver options:

import { buildTypeDefsAndResolvers } from '@lib/type-graphql-query';
import { Product } from './dto/product.type';
import { product as productTable } from '@lib/db';
const { resolvers, typeDefs } = await buildTypeDefsAndResolvers({
resolvers: [
{
DTOClass: Product,
EntityClass: productTable,
delete: {
useSoftDelete: true,
},
},
],
});

Your entity should have a deletedAt field (or similar) to track deletion:

// Example with Drizzle
export const product = pgTable('product', {
id: uuid('id').primaryKey(),
name: varchar('name', { length: 255 }),
price: decimal('price', { precision: 10, scale: 2 }),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
// ... other fields
});

By default, soft-deleted records are excluded from queries:

GraphQL:

{
products {
edges {
node {
id
name
price
}
}
}
}

This query will only return records where deletedAt IS NULL.

Use the withDeleted parameter to include deleted records:

GraphQL:

{
products(withDeleted: true) {
edges {
node {
id
name
price
}
}
}
}

You can filter to show only deleted records:

GraphQL:

{
products(filter: { deletedAt: { isNot: null } }, withDeleted: true) {
edges {
node {
id
name
price
}
}
}
}

When soft delete is enabled, the deleteOne and deleteMany mutations mark records as deleted:

GraphQL:

mutation {
deleteOneProduct(input: { id: "1" }) {
id
name
price
}
}

The record is not removed from the database; instead, its deletedAt field is set to the current timestamp.

You can use @ContextFilter() to automatically exclude soft-deleted records:

import { ObjectType, Field } from '@lib/type-graphql';
import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()
@ContextFilter<Product>((ctx: OperationContext) => {
// Automatically exclude soft-deleted items for read operations
// The withDeleted flag in the query will override this
if (ctx.operationType === 'READ') {
return {
deletedAt: { is: null },
};
}
// For mutations, allow access to soft-deleted items
return undefined;
})
export class Product {
@Field()
id!: string;
@Field()
name!: string;
@Field({ nullable: true })
deletedAt?: Date;
}

To restore a soft-deleted record, you can update it to set deletedAt to null:

GraphQL:

mutation {
updateOneProduct(input: { id: "1", update: { deletedAt: null } }) {
id
name
deletedAt
}
}

If you need to actually delete records from the database, you can:

  1. Disable soft delete for specific resolvers
  2. Use a custom mutation that performs hard delete
  3. Access the QueryService directly to perform hard delete
  1. Use soft delete by default - It provides better data safety and audit trails
  2. Index the deletedAt field - This improves query performance when filtering deleted records
  3. Consider cleanup jobs - Periodically archive or permanently delete old soft-deleted records
  4. Document deletion policies - Make it clear when records are soft-deleted vs hard-deleted
  5. Use withDeleted carefully - Only query deleted records when necessary for admin or audit purposes