Implementing Soft Deletes
1. Adding Deleted Flag
| Column | Type | Use |
|---|---|---|
| isDeleted | boolean | Quick filter |
| deletedAt | timestamp | When deleted (null = active) |
| deletedBy | userId | Audit trail |
2. Adding Deleted Timestamp
Example: Soft-delete schema
type Post {
id: ID!
title: String!
deletedAt: DateTime
deletedBy: User
}
3. Filtering Deleted Records
Example: Default filter
posts: (_, args, { db }) =>
db.post.findMany({
where: { deletedAt: null, ...buildFilter(args.filter) }
});
4. Implementing Restore Mutation
| Mutation | Action |
|---|---|
| restorePost(id) | Set deletedAt = null |
| deletePost(id) | Set deletedAt = now |
| purgePost(id) | Hard delete (admin only) |
5. Implementing Permanent Delete
Example: Hard purge
purgePost: async (_, { id }, ctx) => {
requireRole(ctx, "ADMIN");
await db.post.delete({ where: { id } });
return true;
}
6. Showing Deleted Records
| Pattern | Detail |
|---|---|
| includeDeleted: Boolean = false | Opt-in flag (admin views) |
| trash query | Dedicated deletedPosts field |
| Filter on deletedAt | Expose deletedAt range filter |
7. Using Deleted By Field
| Field | Reason |
|---|---|
| deletedBy | Audit who deleted |
| deletedReason | Optional context |
| deletedAt | Compliance / retention |
8. Cascading Soft Deletes
Example: Cascade transactionally
db.$transaction([
db.post.update({ where: { id }, data: { deletedAt: now, deletedBy: userId } }),
db.comment.updateMany({ where: { postId: id, deletedAt: null }, data: { deletedAt: now } })
]);
9. Archiving Deleted Data
| Step | Detail |
|---|---|
| Move to cold storage | Glacier/archive table |
| Retention | e.g. delete after 30 days in trash |
| Compliance | GDPR right-to-erasure honored |