PHI

HomeDocsDelete Hooks

Delete Hooks

Delete hooks allow you to intercept Delete and DeleteMany operations before queries hit the database. You can enforce safety rules (preventing root user or mass table deletion), restrict deletion targets, or return preloaded relations on deleted records.


Schema Context

The examples in this guide reference the following Prisma schema:

model User {
  id         String   @id @default(cuid())
  email      String   @unique
  phoneNum   String   @unique
  password   String?
  role       UserRole @default(STUDENT)
  loginCount Int      @default(0)
  createdAt  DateTime @default(now())

  posts      Post[]
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  published Boolean  @default(false)
  authorId  String?
}

Registration & Chaining

Register hooks on a model delegate using .Use(), passing a model extension (e.g. user.Extension):

db.User.Use(user.Extension{
    Delete: func(ctx context.Context, args *phi.UserDeleteArgs, next phi.UserDeleteQuery) (*phi.User, error) {
        // Pre-hook: prevent deletion of protected user
        for _, p := range args.Where {
            if p.Column() == user.Email.Column && p.Value() == "admin@example.com" {
                return nil, errors.New("cannot delete primary admin user")
            }
        }

        res, err := next(ctx, args)

        // Post-hook: runs after successful deletion
        return res, err
    },
})

Hook Signatures

Hook Field Signature Return Type
Delete func(ctx context.Context, args *phi.UserDeleteArgs, next phi.UserDeleteQuery) (*phi.User, error)
DeleteMany func(ctx context.Context, args *phi.UserDeleteManyArgs, next phi.UserDeleteManyQuery) (int64, error)

1. Safety Guards & Aborting Deletion

To abort a deletion operation, return an error before invoking next(ctx, args). The database is never queried:

Guarding Against Mass Deletion (`DeleteMany`)

db.User.Use(user.Extension{
    DeleteMany: func(ctx context.Context, args *phi.UserDeleteManyArgs, next phi.UserDeleteManyQuery) (int64, error) {
        // Refuse empty WHERE clause (prevent truncating entire table)
        if len(args.Where) == 0 {
            return 0, errors.New("unbounded bulk delete is rejected for safety")
        }
        return next(ctx, args)
    },
})

2. Restricting Target Predicates (`Where`)

Inspect args.Where or append filters to enforce scoping (e.g., tenant boundaries):

db.User.Use(user.Extension{
    DeleteMany: func(ctx context.Context, args *phi.UserDeleteManyArgs, next phi.UserDeleteManyQuery) (int64, error) {
        // Only allow deleting STUDENT accounts
        args.Where = append(args.Where, user.Role.EQ(phi.UserRole_STUDENT))
        return next(ctx, args)
    },
})

3. Preloading Relations on Deleted Records (`Delete`)

Delete carries args.Select, allowing you to retrieve selected scalar fields and preloaded relations of the record being deleted:

db.User.Use(user.Extension{
    Delete: func(ctx context.Context, args *phi.UserDeleteArgs, next phi.UserDeleteQuery) (*phi.User, error) {
        // Return deleted user along with their posts
        args.Select.Email = true
        args.Select.Posts = post.Query()

        return next(ctx, args)
    },
})