ReviewOS

stacks/bun-query-builder

ORM: soft deletes never filter reads — deleted rows leak into get/find/count/paginate

#1024
Closed glennmichael123 opened this 2 months ago ·
2 months ago

Summary

The useSoftDeletes trait adds a deleted_at column and ModelInstance.delete() correctly sets it, but ModelQueryBuilder never appends deleted_at IS NULL to reads. Every query().get(), find(), count(), paginate(), aggregate, and eager-load returns soft-deleted rows. There is also no withTrashed / onlyTrashed / restore.

Evidence (repro, sqlite)

after p2.delete(): Post.query().get() -> 2 rows ["A","B"]; Post.query().count() -> 2

The deleted row is still returned.

Location

src/orm.ts — buildQuery() (~1888), count() (~2107), aggregate, pluck, and the eager-load paths.

Suggested fix

When definition.traits?.useSoftDeletes (or its softDeletable alias) is set, auto-inject deleted_at IS NULL into all read SQL, and add withTrashed() / onlyTrashed() / restore().

Severity: high (core trait silently broken). Found via internal code audit.

9 days ago

Fixed in d016b46 (on main; next release).

ModelQueryBuilder now injects deleted_at IS NULL into all reads (get/first/count/paginate/aggregate/pluck), with user clauses parenthesised so a top-level OR can't escape the filter. Added withTrashed()/onlyTrashed() (instance + static), find()/findMany() exclude trashed by default, and new ModelInstance.restore() / trashed(). Trait detection accepts the softDeletable alias (write-path alias alignment tracked in #1031). Regression test added; full suite green.

Sign in to comment on this issue.