Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions apps/shop/inventory-service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@
const app = express();
const port = parseInt(process.env.PORT || "80", 10);

type ProductRow = {
id: number;
name: string;
description: string | null;
price_cents: number;
stock: number;
image_url: string | null;
image_urls: unknown;
is_new: boolean;
};

const deprecatedImageReplacements: Record<string, string[]> = {
"Metal Mart/samples/mirrord-hoodie-front": [
"team_Work_makes_the_Dream_Work_-_front_w5qdnb",
"team_work_makes_the_dream_work_-_back_onanux",
],
};

function normalizeImageValue(value: unknown): string[] {
if (typeof value !== "string") return [];

const trimmed = value.trim();
if (!trimmed) return [];

return deprecatedImageReplacements[trimmed] ?? [trimmed];
}

function normalizeImageUrls(imageUrls: unknown, imageUrl: unknown): string[] {
const urls = Array.isArray(imageUrls) ? imageUrls.flatMap(normalizeImageValue) : [];
if (urls.length > 0) return urls;

return normalizeImageValue(imageUrl);
}

function normalizeProductRow(row: ProductRow) {
const image_urls = normalizeImageUrls(row.image_urls, row.image_url);
return {
...row,
image_url: image_urls[0] ?? null,
image_urls,
};
}

let dbUrl = process.env.DATABASE_URL || "postgresql://postgres:postgres@localhost:5432/inventory";
// mirrord branch DB URLs may omit the database name — ensure we connect to "inventory"
if (dbUrl && !/:\d+\/.+$/.test(dbUrl)) {
Expand All @@ -26,36 +69,36 @@
image_url VARCHAR(512),
image_urls JSONB DEFAULT '[]'::jsonb,
is_new BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
try {
await client.query("ALTER TABLE products ADD COLUMN image_url VARCHAR(512)");
} catch (err: unknown) {
if ((err as { code?: string }).code !== "42701") throw err;
}
try {
await client.query("ALTER TABLE products ADD COLUMN image_urls JSONB DEFAULT '[]'::jsonb");

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a database access
, but is not rate-limited.
} catch (err: unknown) {
if ((err as { code?: string }).code !== "42701") throw err;
}
try {
await client.query("ALTER TABLE products ADD COLUMN is_new BOOLEAN DEFAULT false");
} catch (err: unknown) {
if ((err as { code?: string }).code !== "42701") throw err;
}
// Mark first two products as "new" for existing DBs
await client.query("UPDATE products SET is_new = true WHERE id IN (1, 2)");
// Migrate image_url to image_urls for existing rows
await client.query(`
UPDATE products SET image_urls = jsonb_build_array(image_url)
WHERE (image_urls IS NULL OR image_urls = '[]'::jsonb) AND image_url IS NOT NULL
`);
} finally {
client.release();
}
}

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a database access
, but is not rate-limited.
app.use(express.json());

app.use((req, _res, next) => {
Expand All @@ -72,8 +115,8 @@
app.get("/products", async (_req, res) => {
// Set a breakpoint here; trigger with: curl http://localhost:28080/products -H "X-PG-Tenant: dev" (while port-forward + mirrord are running)
try {
const { rows } = await pool.query("SELECT id, name, description, price_cents, stock, image_url, image_urls, is_new FROM products ORDER BY id");
res.json(rows);
const { rows } = await pool.query<ProductRow>("SELECT id, name, description, price_cents, stock, image_url, image_urls, is_new FROM products ORDER BY id");
res.json(rows.map(normalizeProductRow));
} catch (err) {
console.error("Error fetching products:", err);
res.status(500).json({ error: "Internal server error" });
Expand All @@ -86,14 +129,14 @@
return res.status(400).json({ error: "Invalid product ID" });
}
try {
const { rows } = await pool.query(
const { rows } = await pool.query<ProductRow>(
"SELECT id, name, description, price_cents, stock, image_url, image_urls, is_new FROM products WHERE id = $1",
[id]
);
if (rows.length === 0) {
return res.status(404).json({ error: "Product not found" });
}
res.json(rows[0]);
res.json(normalizeProductRow(rows[0]));
} catch (err) {
console.error("Error fetching product:", err);
res.status(500).json({ error: "Internal server error" });
Expand Down
16 changes: 11 additions & 5 deletions apps/shop/metal-mart-frontend/src/lib/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,23 @@ export type Product = {
is_new?: boolean;
};

function normalizeImageUrl(url: string | null | undefined): string | null {
const trimmed = url?.trim();
return trimmed ? trimmed : null;
}

/** Primary image for thumbnails (first in array, or legacy image_url). */
export function getPrimaryImageUrl(product: Product): string | null {
const urls = product.image_urls;
if (urls && urls.length > 0) return urls[0];
return product.image_url ?? null;
return getImageUrls(product)[0] ?? null;
}

/** All image URLs for product (front, back, etc.). */
export function getImageUrls(product: Product): string[] {
const urls = product.image_urls;
const urls = product.image_urls
?.map((url) => normalizeImageUrl(url))
.filter((url): url is string => url !== null);
if (urls && urls.length > 0) return urls;
const single = product.image_url;

const single = normalizeImageUrl(product.image_url);
return single ? [single] : [];
}
Loading