// Shared item SELECT shapes for the public and admin routes. // // Images and tags are pulled as scalar subqueries rather than LEFT JOIN + // GROUP BY. Joining two one-to-many relations in the same query multiplies // their rows together — an item with 2 images and 3 tags would aggregate 6 // rows, silently repeating every image three times. Subqueries keep each // aggregate independent and drop the GROUP BY entirely. const IMAGES_SUBQUERY = ` COALESCE(( SELECT json_agg(json_build_object('id', img.id, 'image_path', img.image_path, 'sort_order', img.sort_order) ORDER BY img.sort_order) FROM item_images img WHERE img.item_id = i.id ), '[]') AS images`; const TAGS_SUBQUERY = ` COALESCE(( SELECT json_agg(json_build_object('id', t.id, 'name', t.name, 'color', t.color) ORDER BY t.name) FROM item_tags it JOIN tags t ON t.id = it.tag_id WHERE it.item_id = i.id ), '[]') AS tags`; const FROM_CLAUSE = ` FROM items i LEFT JOIN categories c ON c.id = i.category_id`; // The storefront gets an explicit column list — it has no business seeing // paypal_order_id or reserved_until. export const PUBLIC_ITEM_SELECT = ` SELECT i.id, i.name, i.description, i.price_cents, i.status, i.created_at, i.category_id, c.name AS category_name, ${IMAGES_SUBQUERY}, ${TAGS_SUBQUERY} ${FROM_CLAUSE}`; export const ADMIN_ITEM_SELECT = ` SELECT i.*, c.name AS category_name, ${IMAGES_SUBQUERY}, ${TAGS_SUBQUERY} ${FROM_CLAUSE}`;