fix(admin): answer 404 for an item id that does not exist (#207)
Three routes in admin.ts answered a miss with a success. PUT /items/:id ran an UPDATE that matched nothing, committed happily, selected nothing back and replied 200 with an empty body — a success the admin client could do nothing with, and no record anywhere that the item was not found. mark-sold and mark-available did the same. The create route beside them has always used requireRow for exactly this, which is why this reads as an oversight rather than a decision.
A garbage id was worse in a different direction. Number('abc') is NaN, the driver sends it to Postgres as the text "NaN", Postgres raises 22P02 for an integer column, and the catch turned that into a 500 — so a caller asking for an item that cannot exist was told the server broke. Both now answer 404, because from the caller's side "/items/abc" identifies no item in exactly the way "/items/999999" does.
readId is shared rather than repeated, and rejects zero, negatives and fractions as well as text: every id in this schema is a positive serial, so anything else identifies nothing.
mark-sold now notifies favouriters only after the row is known to exist, so nobody is told about a sale that did not happen.
The issue asked for the same shape to be checked across the other admin routes. It was: unpublish already looks the item up and 404s, and the tags and categories PUT routes both do an existence check before their UPDATE, so their rows[0] is guaranteed. items.ts already guards the public read. These three were the only ones lying about a miss.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,3 +91,21 @@ export function trimTrailingSlashes(value: string): string {
|
||||
while (trimmed.endsWith('/')) trimmed = trimmed.slice(0, -1);
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* A route's `:id` as a positive integer, or null when it is not one.
|
||||
*
|
||||
* Guarding this is not cosmetic. `Number('abc')` is NaN, which the driver sends
|
||||
* to Postgres as the text "NaN"; Postgres raises 22P02 for an integer column,
|
||||
* the route's catch turns that into a 500, and a caller asking for an item that
|
||||
* cannot exist is told the server broke. Returning null lets the route answer
|
||||
* 404, which is what "/items/abc" actually means. See #207.
|
||||
*
|
||||
* Rejects 0 and negatives as well as fractions: every id in this schema is a
|
||||
* positive serial, so anything else identifies nothing.
|
||||
*/
|
||||
export function readId(value: string | undefined): number | null {
|
||||
if (value === undefined || value.trim() === '') return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user