Rebasely Beta: data may be purged

examples/storefront Public

TypeScript · 86 lines · 4.0 KiB · at 38a9ec4

// Carts: a shopper's items before checkout. A cart is open until it is checked out.import { HttpError } from "./http.ts";import type { ImageUrls } from "./images.ts";import { totals, type Totals } from "./money.ts";export const MAX_QUANTITY = 10;export const MAX_LINES = 50;export type CartItem = {  product_id: string;  name: string;  quantity: number;  unit_price_cents: number;  image_url: string | null;};export type Cart = { id: string; status: "open" | "checked_out"; items: CartItem[] } & Totals;type ItemRow = { product_id: string; name: string; quantity: number; unit_price_cents: number; image_key: string | null };export async function createCart(db: D1Database, id: string, now: number, images: ImageUrls): Promise<Cart> {  await db.prepare("INSERT INTO carts (id, created_at, updated_at) VALUES (?, ?, ?)").bind(id, now, now).run();  return (await getCart(db, id, images))!;}export async function getCart(db: D1Database, id: string, images: ImageUrls): Promise<Cart | null> {  const cart = await db.prepare("SELECT id, status FROM carts WHERE id = ?").bind(id).first<{ id: string; status: Cart["status"] }>();  if (!cart) return null;  const { results } = await db    .prepare(      `SELECT ci.product_id, p.name, ci.quantity, ci.unit_price_cents, p.image_key       FROM cart_items ci JOIN products p ON p.id = ci.product_id       WHERE ci.cart_id = ? ORDER BY ci.added_at, ci.product_id`,    )    .bind(id)    .all<ItemRow>();  const items = results.map((r) => ({    product_id: r.product_id,    name: r.name,    quantity: r.quantity,    unit_price_cents: r.unit_price_cents,    image_url: r.image_key ? images(r.image_key) : null,  }));  return { id: cart.id, status: cart.status, items, ...totals(items) };}async function openCart(db: D1Database, id: string): Promise<void> {  const cart = await db.prepare("SELECT status FROM carts WHERE id = ?").bind(id).first<{ status: string }>();  if (!cart) throw new HttpError(404, "cart_not_found", "No such cart.");  if (cart.status !== "open") throw new HttpError(409, "cart_closed", "This cart has been checked out.");}// Sets how many of a product the cart holds; 0 removes it. The price is the product's price now.export async function setQuantity(db: D1Database, cartId: string, productId: string, quantity: unknown, now: number, images: ImageUrls): Promise<Cart> {  if (!Number.isInteger(quantity) || (quantity as number) < 0 || (quantity as number) > MAX_QUANTITY) {    throw new HttpError(400, "bad_quantity", `quantity is a whole number from 0 to ${MAX_QUANTITY}.`);  }  await openCart(db, cartId);  if (quantity === 0) {    await db.prepare("DELETE FROM cart_items WHERE cart_id = ? AND product_id = ?").bind(cartId, productId).run();  } else {    const product = await db      .prepare("SELECT price_cents, stock FROM products WHERE id = ? AND active = 1")      .bind(productId)      .first<{ price_cents: number; stock: number }>();    if (!product) throw new HttpError(404, "product_not_found", "No such product.");    if (product.stock < (quantity as number)) {      throw new HttpError(409, "out_of_stock", "There are not that many left.", { product_id: productId, available: product.stock });    }    const lines = await db      .prepare("SELECT COUNT(*) AS n FROM cart_items WHERE cart_id = ? AND product_id != ?")      .bind(cartId, productId)      .first<number>("n");    if ((lines ?? 0) >= MAX_LINES) throw new HttpError(409, "cart_full", `A cart holds at most ${MAX_LINES} different products.`);    await db      .prepare(        `INSERT INTO cart_items (cart_id, product_id, quantity, unit_price_cents, added_at) VALUES (?, ?, ?, ?, ?)         ON CONFLICT (cart_id, product_id) DO UPDATE SET quantity = excluded.quantity, unit_price_cents = excluded.unit_price_cents`,      )      .bind(cartId, productId, quantity, product.price_cents, now)      .run();  }  await db.prepare("UPDATE carts SET updated_at = ? WHERE id = ?").bind(now, cartId).run();  return (await getCart(db, cartId, images))!;}