plaintext sql typescript 116 lines · 4 tabs

Soft-Delete and Restore in TypeScript with a Prisma Repository and Migration

Shared by codesnips Aug 2026
4 tabs
model User {
  id        String    @id @default(uuid())
  email     String    @unique
  name      String
  posts     Post[]
  deletedAt DateTime?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  @@index([deletedAt])
}

model Post {
  id        String    @id @default(uuid())
  title     String
  body      String
  authorId  String
  author    User      @relation(fields: [authorId], references: [id])
  deletedAt DateTime?
  createdAt DateTime  @default(now())

  @@index([deletedAt])
}
4 files · plaintext, sql, typescript Explain with highlit

Soft deletion keeps rows in the database and marks them as deleted instead of physically removing them, which preserves referential history, enables restore, and supports auditing. This snippet shows a focused implementation built around a Prisma schema, a raw SQL migration, and a generic repository base class that hides the soft-delete mechanics from callers.

In schema.prisma, both the User and Post models carry a nullable deletedAt DateTime? column. A null value means the record is live; a timestamp means it was deleted at that instant. Using a timestamp instead of a boolean flag records when the deletion happened, which is valuable for retention windows and audit trails. The @@index([deletedAt]) matters because nearly every query filters on this column, and without an index those filters degrade as the table grows.

The soft_delete migration adds the column and, critically, creates a partial index (WHERE deleted_at IS NULL). A partial index is smaller and faster than a full index because it only covers live rows — the exact set queried most often — while the archived rows sit outside the hot path. The migration is written as plain SQL so the deployed schema is explicit and reviewable.

SoftDeleteRepository is the heart of the pattern. It is generic over a Prisma delegate type and centralises three behaviours so no caller ever forgets them. findMany and findById inject deletedAt: null into the where clause, so deleted rows are invisible by default. softDelete sets deletedAt to the current time rather than issuing a real DELETE, and restore clears it back to null. An explicit hardDelete remains available for compliance-driven purges. Concentrating the filtering logic here avoids the most common soft-delete bug: a stray query that forgets the filter and leaks tombstoned records into a normal listing.

UsersService shows the payoff. It extends the base repository by passing prisma.user to super, and its own methods read cleanly — deactivate calls softDelete, reactivate calls restore — with no deletedAt bookkeeping in sight. The trade-off is that unique constraints still apply to soft-deleted rows, so reusing a deleted user's email may require either a composite unique index including deletedAt or a pre-check. This approach fits systems needing recoverability and audit history; when storage or privacy law demands true erasure, hardDelete is the escape hatch.


Related snips

Share this code

Here's the card — post it anywhere.

Soft-Delete and Restore in TypeScript with a Prisma Repository and Migration — share card
Link copied