Files
redefined-designs/backend/src/routes/items.ts
T
bermudalambandClaude Opus 5 c77fdad2b9 fix: run migrations on boot and stop failures rendering as empty (#23)
The storefront showed no inventory after deploying the categories/tags
release. No data was lost: the code queried categories/item_tags/
items.category_id against a database where the migration had not been
run, and that failure was invisible at every layer.

Three changes, each addressing one layer:

Migrations now run at container start, so deployed code cannot be ahead
of the schema and the easily-forgotten manual `docker exec migrate.js
up` step disappears. migrate.js waits for Postgres to accept
connections first, since the NAS brings the DB container up slower than
the app, and still exits non-zero so a bad migration stops the
container rather than serving a half-migrated schema.

Express 4 does not forward a rejected async handler, and no error
middleware was mounted, so a failing query never responded at all. Async
routes are now wrapped and an error middleware guarantees a 500. A hung
request is indistinguishable from an empty result in the UI, which is
how a schema mismatch came to read as "the store has no items".

The storefront now separates "request failed" from "no items" and offers
a retry. fetchItems/fetchFilterOptions throw on a non-OK response rather
than returning the parsed error body, which would have been set as the
item list and crashed the grid on .map.

Also restores the project-context update from 7fb5764, which was left
out of PR #24 and ended up dangling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:41:24 -05:00

35 lines
1.3 KiB
TypeScript
Executable File

import { Router, Request, Response } from 'express';
import { pool } from '../db';
import { asyncRoute } from '../asyncRoute';
import { PUBLIC_ITEM_SELECT } from '../itemSelect';
import { parseItemFilters, buildItemFilterSql, FilterError } from '../itemFilters';
const router = Router();
router.get('/', asyncRoute(async (req: Request, res: Response) => {
let filters;
try {
filters = parseItemFilters(req.query as Record<string, unknown>);
} catch (err) {
// A malformed filter is returned as an error rather than ignored, so a
// broken link shows itself instead of quietly listing the whole catalogue.
if (err instanceof FilterError) {
return res.status(400).json({ error: err.message });
}
throw err;
}
const { clauses, params } = buildItemFilterSql(filters, 1);
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
const { rows } = await pool.query(`${PUBLIC_ITEM_SELECT} ${where} ORDER BY i.created_at DESC`, params);
res.json(rows);
}));
router.get('/:id', asyncRoute(async (req: Request, res: Response) => {
const { rows } = await pool.query(`${PUBLIC_ITEM_SELECT} WHERE i.id = $1`, [req.params.id]);
if (!rows.length) return res.status(404).json({ error: 'not found' });
res.json(rows[0]);
}));
export default router;