Repository navigation
get() hides soft-deleted records unless includeDeleted, as query() does #368
Description
Problem
query() hides soft-deleted records by default. get() returns them with deletedAt set, and there's no option to change that, so every app needs its own deletedAt guard after a get().
The spec also disagrees with the code in the same area. versioning.md § Version history says a tombstone "is read back with get(id, { includeDeleted: true })", but GetRecordOptions only has presentAt. The same paragraph says delete() "returns nothing", but it returns DeleteResult ({ referencedFileIds }).
Decision
get() hides soft-deleted records by default, as query() does. get(id) on a tombstone returns null. get(id, { includeDeleted: true }) returns it, still as the tombstone projection under ScopedStack. That makes the spec's includeDeleted sentence true. Separately, the spec's delete() wording is corrected.
Why
- An app developer doesn't expect deleted records back without asking for them.
- One rule for both reads: soft-deleted records are hidden unless you pass
includeDeleted. nullrather than a throw:get()is the nullable read, and a tombstone is "not here" as far as normal reads are concerned.- Any reader may pass
includeDeleted, as onquery(). The record's own permissions still decide whether they see it, so the flag leaks nothing. - Writes stay explicit.
mutate()on a tombstone still throws "soft-deleted; undelete it first". Reads hide deleted records and writes explain why they're refused. - On
delete(): it returnsDeleteResult, whichattachments.mdalready documents along withdeleteAndReturn(). The reasonversioning.mdgives still holds: a purge leaves no Record behind, sodelete()is the one mutating verb that doesn't answer with one. It answers with a report instead.
Wire: GET /records/:id answers 404 for a tombstone unless called with ?includeDeleted=true. That fits the disclosure rule, because 404 already means "missing or unreadable", and a requester who can read the record can opt in to see that it's deleted.
Risk: core's own lookups must opt in. ScopedStack calls this.stack.get(id) in five places, including requireVerb(), which undelete(), the tombstone 409 and history reads all go through. If those get null back, undelete() breaks and a mutation on a tombstone answers 404 instead of 409. Every internal lookup that's supposed to see tombstones has to pass includeDeleted: true.
Actions
- Add
includeDeleted?: booleantoGetRecordOptions(packages/core/src/stack.ts) and defaultStack.get()andScopedStack.get()to hiding tombstones. - Audit every internal
get()caller inStackandScopedStack, including the tombstone refusal inStackfrom Unscoped Stack lets you edit a tombstone; ScopedStack refuses #367, and passincludeDeleted: truewherever a tombstone is a valid target. - Tests on both
StackandScopedStack:get()returnsnullfor a tombstone.includeDeletedreturns it (as the tombstone projection underScopedStack).undelete()still works.- Mutating a tombstone is still
409, and a stranger still gets404. getVersions()andgetJournal()still serve a deleted record's history.
- Wire:
GET /records/:idtakes?includeDeleted=. Updatewire-format.md(the "served as a tombstone" paragraph under§ Records),APIAdapter.get()and@haverstack/conformance-fixturestogether. - Spec:
- State the default in
data-model.mdnext to the query filter'sincludeDeleted, and inversioning.md § Deletion/§ The tombstone is literal. - In
versioning.md § Version history, replace "delete()is the one that returns nothing" with:delete()returns aDeleteResultrather than a Record, anddeleteAndReturn()is the call to use when the caller needs the tombstone or the purged body. Theget(id, { includeDeleted: true })sentence stays as written. - Check
docs/spec.md("delete()excepted") for the same wording.
- State the default in
- Changesets:
minorfor@haverstack/core,@haverstack/adapter-apiand@haverstack/conformance-fixtures.
Order
Blocked by #367: the tests here assert that mutating a tombstone is 409 on both surfaces, which is only true once #367 lands, and the audit should cover the check #367 adds to Stack.
Activity
feat(core): get() hides soft-deleted records unless includeDeleted