Fullstack CourseLearn by building
Back to week 4

Topic

Challenge: broken pagination / soft-delete leak

Definition

A soft-delete leak occurs when a query intended to exclude logically deleted rows omits the deletion filter, causing removed records to still appear in lists, counts, or pagination totals.

In simpler words

Deleted tickets are not really gone from the database. If a query forgets to filter them out, they reappear in lists and mess up page counts.

Combines pagination correctness with a realistic soft-delete pitfall.

After this you can

  • Explain why soft delete requires filtering in every read query, not only the list
  • Spot a pagination total that disagrees with the visible rows
  • Choose a fix that keeps the data query and the count query consistent

Why soft-delete leaks are easy to introduce

Definition

Soft delete marks a row as removed, for example with a deletion timestamp, without a physical delete, so every subsequent read query must explicitly exclude it, unlike a hard delete which removes the row from all future query results automatically.

In simpler words

Marking a row deleted does not stop it from showing up unless every query remembers to check for that mark.

A new query, index, or export path added later can easily forget the exclusion condition older queries already had. The leak often appears in only one endpoint, not all of them.

When the count query and the data query use different filters, the pagination total disagrees with what a user actually sees, producing an empty last page or missing rows.

Keep in mind

  • Every read query on a soft-deletable entity needs the same exclusion filter; factor it into one shared place.
  • A pagination total and the data query must share filters or pagination math breaks.
  • A leak that shows up only on later pages is a strong hint the count and data queries disagree.

Challenge lab

Bug report

Deleted tickets still show up on later pages of the tickets list, and the last page is sometimes empty.

What is broken

The paginated data query filters out soft-deleted tickets, but the separate count query used for the pagination total does not, so the total overcounts and the page math misaligns.

Broken snippet

async list(page: number, limit: number) {
  const [data] = await this.ticketsRepo.findAndCount({
    where: { deletedAt: null },
    skip: (page - 1) * limit,
    take: limit,
  });
  const total = await this.ticketsRepo.count(); // forgot the deletedAt filter
  return { data, meta: { page, limit, total } };
}

Pass criteria

  • Soft-deleted tickets never appear in list results
  • The pagination total matches the same filter used for the actual page of data
  • The pagination page count is consistent with what a user can actually page through

Checking your session…

Choose the correct fix

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥80% (enforced by the API) to save progress.

Checking your session…

15 questions · concept 5 · syntax 3 · practical 4 · logic 3

1. How should soft-delete pagination be fixed?
Concept
2. Why do mismatched count/data break UX?
Syntax
3. Why not filter only in the browser?
Practical
4. Why not inflate totals with soft-deleted rows?
Logic
5. What shared approach helps?
Concept
6. Does increasing page size fix soft-delete math?
Practical
7. Deleted rows leak onto later pages and the last page is sometimes empty. What is the fix?
Syntaxadvanced
const [data] = await repo.findAndCount({
  where: { deletedAt: null },
  skip: (page - 1) * limit,
  take: limit,
});
const total = await repo.count(); // total
8. How do soft deletes interact with page?
Logic
9. Which test catches the bug?
Concept
10. Which approach is wrong?
Practical
11. How does a shared where helper prevent pagination bugs?
Conceptintermediate
function activeWhere() {
  return { deletedAt: IsNull() };
}
12. Why does the last page render empty?
Syntaxadvanced
// total=100 (includes deleted), 10 deleted, limit=10
// page 10 requested
13. For a normal list, is this default filter correct?
Practicalintermediate
repo.findAndCount({ take, skip }); // default excludes soft-deleted
14. Why is bumping the page size not a real fix?
Logicadvanced
limit = 1000; // "fix" so deletes are less noticeable
15. What class of bug does asserting data vs meta catch?
Conceptintermediate
expect(res.data.length).toBeLessThanOrEqual(res.meta.total);

Checking your session…