Start learning free

LESSON 168 OF 173 · BACKEND: BACKEND WITH AI

Project: Review an AI Pull Request

Practice

An AI assistant opened a pull request for the shop: customers can send a discount code to POST /products/:productId/discount and get the price after the discount, in cents. It works for SPRING10 on the Lamp, so it looks done. Here is the AI's route:

app.post("/products/:productId/discount", (req: Request, res: Response) => {
  const productId = Number(req.params.productId);
  const data = req.body;
  const product = db.prepare("SELECT * FROM products WHERE id = ?").get(productId) as Product;
  const discount = db.prepare("SELECT * FROM discounts WHERE code = '" + data.code + "'").get() as Discount;
  const totalCents = product.price * (100 - discount.percent);
  res.json({ product: product.name, code: discount.code, total_cents: totalCents });
});

A review asks what the AI never tried. Missing: product 99 or the code SUMMER50 gives undefined, and the route crashes with 500. The AI wrote as Product instead of as Product | undefined, so TypeScript never warned that the row might be missing. Hostile: the + glues the customer's text into the SQL, so the code x' OR '1'='1 matches every row and gets SPRING10's discount. That is SQL injection. Twice: every code has uses_left, and the AI never reads it, so the one-use VIP25 works forever and the used-up WINTER15 still works.

You know each fix from earlier lessons. Here they are on another table: a ? placeholder keeps typed text a plain value, and one UPDATE with a condition takes a unit only if one is left. Then result.changes tells you whether it did.

const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email) as User | undefined;
const result = db.prepare("UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0").run(productId);
if (result.changes === 0) {
  return res.status(409).json({ error: "Out of stock" });
}

The codes: SPRING10 (10% off, 100 uses), VIP25 (25% off, 1 use) and WINTER15 (15% off, used up). Sent in this order, the requests must get these answers:

POST /products/3/discount   {"code": "SPRING10"}      →  200  {"code": "SPRING10", "product": "Lamp", "total_cents": 2610}
POST /products/99/discount  {"code": "SPRING10"}      →  404  {"error": "Product not found"}
POST /products/3/discount   {"code": "SUMMER50"}      →  404  {"error": "Unknown code"}
POST /products/3/discount   {"code": "x' OR '1'='1"}  →  404  {"error": "Unknown code"}
POST /products/1/discount   {"code": "VIP25"}         →  200  {"code": "VIP25", "product": "Mug", "total_cents": 900}
POST /products/1/discount   {"code": "VIP25"}         →  409  {"error": "Code used up"}
POST /products/5/discount   {"code": "WINTER15"}      →  409  {"error": "Code used up"}

Your task: the database, the discounts table, the Product and Discount types, the route and its first line (the id from the path as a number, in productId) are ready. Write the rest of the route so it fixes all three problems: 404 for a missing product or code, a ? placeholder for the code, and one atomic UPDATE that takes a use, with 409 when none is left. Keep the AI's total in cents.


Assignment
Follow the Coding Tutorial to rewrite the discount route one problem at a time: the lookups, the two 404s, the placeholder, the atomic UPDATE with its 409, and the answer.


Hint
Check in this order: the product, then the code, then the use, then the price. Take a use only after both lookups found a row, so a request for a missing product never spends a code. End each lookup with as Product | undefined or as Discount | undefined, so TypeScript reminds you to check for the missing row.


STEP 1 OF 10
    Ctrl + Enter to check