Rebasely Beta: data may be purged

examples/storefront Public

TypeScript · 96 lines · 3.4 KiB · at 7b21f05

// The product catalog: what the shop sells, at what price, and how many are left.import { HttpError } from "./http.ts";export type Product = {  id: string;  name: string;  description: string;  price_cents: number;  stock: number;  image_url: string | null;  active: boolean;};type Row = {  id: string;  name: string;  description: string;  price_cents: number;  stock: number;  image_key: string | null;  active: number;};const COLUMNS = "id, name, description, price_cents, stock, image_key, active";const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;export function toProduct(r: Row): Product {  return {    id: r.id,    name: r.name,    description: r.description,    price_cents: r.price_cents,    stock: r.stock,    image_url: r.image_key ? `/images/${r.image_key}` : null,    active: r.active === 1,  };}// Active products by name. The first 100 for now; a catalog bigger than that needs pages.export async function listProducts(db: D1Database): Promise<Product[]> {  const { results } = await db    .prepare(`SELECT ${COLUMNS} FROM products WHERE active = 1 ORDER BY name, id LIMIT 100`)    .all<Row>();  return results.map(toProduct);}// Any product, active or not. Shopper routes check `active` themselves.export async function getProduct(db: D1Database, id: string): Promise<Product | null> {  const row = await db.prepare(`SELECT ${COLUMNS} FROM products WHERE id = ?`).bind(id).first<Row>();  return row ? toProduct(row) : null;}export type ProductInput = {  id: string;  name: string;  description: string;  price_cents: number;  stock: number;  active: boolean;};export function parseProductInput(id: string, body: Record<string, unknown>): ProductInput {  if (!SLUG.test(id) || id.length > 64) {    throw new HttpError(400, "bad_id", "A product id is a lowercase slug of at most 64 characters.");  }  const { name, description = "", price_cents, stock = 0, active = true } = body;  if (typeof name !== "string" || !name.trim() || name.length > 120) {    throw new HttpError(400, "bad_name", "name is a string of 1 to 120 characters.");  }  if (typeof description !== "string" || description.length > 2000) {    throw new HttpError(400, "bad_description", "description is a string of at most 2,000 characters.");  }  if (!Number.isInteger(price_cents) || (price_cents as number) <= 0) {    throw new HttpError(400, "bad_price", "price_cents is a whole number of cents above zero.");  }  if (!Number.isInteger(stock) || (stock as number) < 0) {    throw new HttpError(400, "bad_stock", "stock is a whole number, zero or more.");  }  if (typeof active !== "boolean") throw new HttpError(400, "bad_active", "active is true or false.");  return { id, name: name.trim(), description, price_cents: price_cents as number, stock: stock as number, active };}export async function upsertProduct(db: D1Database, p: ProductInput, now: number): Promise<Product> {  await db    .prepare(      `INSERT INTO products (id, name, description, price_cents, stock, active, created_at, updated_at)       VALUES (?, ?, ?, ?, ?, ?, ?, ?)       ON CONFLICT (id) DO UPDATE SET         name = excluded.name, description = excluded.description, price_cents = excluded.price_cents,         stock = excluded.stock, active = excluded.active, updated_at = excluded.updated_at`,    )    .bind(p.id, p.name, p.description, p.price_cents, p.stock, p.active ? 1 : 0, now, now)    .run();  return (await getProduct(db, p.id))!;}