feat(api): categories, tags, and storefront item filters (#23)

Adds a self-referencing categories tree, a tag registry with
deterministic colours, and item_tags, plus admin CRUD for both.

GET /api/items now accepts category, tags, min_price and max_price.
Category matching walks the subtree with a recursive CTE so selecting a
parent includes everything filed beneath it; tags match with AND via a
count check, since ANY() alone would return items carrying only one of
them. Malformed filter params return 400 rather than being ignored, so a
broken link doesn't quietly list the whole catalogue.

GET /api/filters serves the drawer its tree, tags, and price bounds in
one request.

Item image/tag aggregation moves from LEFT JOIN + GROUP BY to scalar
subqueries. Joining two one-to-many relations multiplies their rows, so
an item with 2 images and 3 tags would have repeated every image three
times once tags were added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 09:13:51 -05:00
co-authored by Claude Opus 5
parent 766358a9fe
commit 9222e97deb
14 changed files with 1288 additions and 29 deletions
+6
View File
@@ -6,6 +6,9 @@ import { router as cartCheckoutRouter, webhookRouter as cartCheckoutWebhookRoute
import adminRouter from './routes/admin';
import adminCustomersRouter from './routes/adminCustomers';
import adminSettingsRouter from './routes/adminSettings';
import adminCategoriesRouter from './routes/adminCategories';
import adminTagsRouter from './routes/adminTags';
import filtersRouter from './routes/filters';
import customersRouter from './routes/customers';
import publicRouter from './routes/public';
import cartRouter from './routes/cart';
@@ -35,10 +38,13 @@ app.get('/api/config', (_req, res) => {
});
app.use('/api/items', itemsRouter);
app.use('/api/filters', filtersRouter);
app.use('/api/cart', cartRouter);
app.use('/api/checkout/cart', cartCheckoutRouter);
app.use('/api/admin/customers', adminCustomersRouter);
app.use('/api/admin/settings', adminSettingsRouter);
app.use('/api/admin/categories', adminCategoriesRouter);
app.use('/api/admin/tags', adminTagsRouter);
app.use('/api/admin', adminRouter);
app.use('/api/customers/me/addresses', shippingAddressesRouter);
app.use('/api/customers', customersRouter);