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>
78 lines
3.2 KiB
TypeScript
Executable File
78 lines
3.2 KiB
TypeScript
Executable File
import express, { Request, Response, NextFunction } from 'express';
|
|
import cookieParser from 'cookie-parser';
|
|
import path from 'path';
|
|
import itemsRouter from './routes/items';
|
|
import { router as cartCheckoutRouter, webhookRouter as cartCheckoutWebhookRouter } from './routes/cartCheckout';
|
|
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';
|
|
import shippingAddressesRouter from './routes/shippingAddresses';
|
|
import { attachCustomer } from './middleware/customerAuth';
|
|
|
|
const app = express();
|
|
// Express advertises itself in X-Powered-By by default, which hands an
|
|
// attacker the server stack for free when picking exploits to try.
|
|
app.disable('x-powered-by');
|
|
app.set('trust proxy', 1);
|
|
|
|
app.use('/webhooks/paypal', express.json(), cartCheckoutWebhookRouter);
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
app.use(attachCustomer);
|
|
app.use('/uploads', express.static(process.env.UPLOADS_DIR || '/app/uploads'));
|
|
|
|
app.get('/api/config', (_req, res) => {
|
|
const clientId = process.env.PAYPAL_CLIENT_ID;
|
|
const isPlaceholder = !clientId || clientId.length < 10 || clientId === 'REPLACE_WITH_PAYPAL_CLIENT_ID';
|
|
res.json({
|
|
paypalClientId: isPlaceholder ? null : clientId,
|
|
demoMode: process.env.DEMO_MODE !== 'false',
|
|
currency: process.env.SITE_CURRENCY || 'USD'
|
|
});
|
|
});
|
|
|
|
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);
|
|
app.use('/', publicRouter);
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
const staticDir = path.join(__dirname, '..', 'public');
|
|
app.use(express.static(staticDir));
|
|
app.get('*', (_req, res) => {
|
|
res.sendFile(path.join(staticDir, 'index.html'));
|
|
});
|
|
}
|
|
|
|
// Mounted last, so it sees errors from every route above. Without it, a route
|
|
// that hands an error to next() falls through to Express's default handler,
|
|
// and — worse — an async route that rejects never responds at all, leaving the
|
|
// client hanging. A hung request is indistinguishable from an empty catalogue
|
|
// in the UI, which is exactly how a schema mismatch once read as "the store
|
|
// has no items". Always answer.
|
|
//
|
|
// The four-argument signature is what marks this as error middleware; `next`
|
|
// is unused but must stay for Express to recognise it.
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
console.error(err);
|
|
if (res.headersSent) return;
|
|
res.status(500).json({ error: 'internal error' });
|
|
});
|
|
|
|
export default app;
|