examples/storefront Public
// The storefront's HTTP API. createApp takes the environment and the few things tests replace// (the clock and id generation) and returns a fetch handler.import { createCart, getCart, setQuantity } from "./carts.ts";import { getProduct, listProducts, parseLimit, parseProductInput, upsertProduct } from "./catalog.ts";import { checkout } from "./checkout.ts";import type { Env } from "./env.ts";import { errorResponse, HttpError, json, readJson, requireAdmin } from "./http.ts";import { imageUrls, putProductImage } from "./images.ts";import { getOrder } from "./orders.ts";import { stripePayments, type Payments } from "./payments.ts";import { Router } from "./router.ts";import { indexProduct, parseQuery, rebuildIndex, searchProducts } from "./search.ts";export type Deps = { now: () => number; newId: () => string; payments: (env: Env) => Payments;};export const defaultDeps: Deps = { now: Date.now, newId: () => crypto.randomUUID(), payments: (env) => stripePayments(env.STRIPE_SECRET_KEY),};export function createApp(env: Env, deps: Deps = defaultDeps) { const r = new Router(); const images = imageUrls(env.IMAGES_URL); r.on("GET", "/products", async (req) => { const q = new URL(req.url).searchParams; return json(await listProducts(env.DB, images, parseLimit(q.get("limit")), q.get("after"))); }); r.on("GET", "/search", async (req) => { const params = new URL(req.url).searchParams; const q = parseQuery(params.get("q")); return json({ query: q, products: await searchProducts(env.DB, env.SEARCH, images, q, parseLimit(params.get("limit"))) }); }); r.on("GET", "/products/:id", async (_req, p) => { const product = await getProduct(env.DB, p.id!, images); if (!product || !product.active) throw new HttpError(404, "not_found", "No such product."); return json({ product }); }); r.on("PUT", "/admin/products/:id", async (req, p) => { requireAdmin(req, env.ADMIN_TOKEN); const input = parseProductInput(p.id!, await readJson(req)); const product = await upsertProduct(env.DB, input, deps.now(), images); try { await indexProduct(env.SEARCH, product); } catch (e) { console.error(JSON.stringify({ msg: "indexing the product failed", product: product.id, error: String(e) })); throw new HttpError(503, "search_index_failed", "The product was saved, but the search index was not updated. Send the same request again."); } return json({ product }); }); r.on("POST", "/admin/search/rebuild", async (req) => { requireAdmin(req, env.ADMIN_TOKEN); return json({ indexed: await rebuildIndex(env.DB, env.SEARCH) }); }); r.on("PUT", "/admin/products/:id/image", async (req, p) => { requireAdmin(req, env.ADMIN_TOKEN); await putProductImage(env.DB, env.IMAGES, p.id!, req, deps.now()); return json({ product: await getProduct(env.DB, p.id!, images) }); }); r.on("POST", "/carts", async () => json({ cart: await createCart(env.DB, deps.newId(), deps.now(), images) }, 201)); r.on("GET", "/carts/:id", async (_req, p) => { const cart = await getCart(env.DB, p.id!, images); if (!cart) throw new HttpError(404, "cart_not_found", "No such cart."); return json({ cart }); }); r.on("PUT", "/carts/:id/items/:product", async (req, p) => { const body = await readJson(req); return json({ cart: await setQuantity(env.DB, p.id!, p.product!, body.quantity, deps.now(), images) }); }); r.on("DELETE", "/carts/:id/items/:product", async (_req, p) => json({ cart: await setQuantity(env.DB, p.id!, p.product!, 0, deps.now(), images) })); r.on("POST", "/carts/:id/checkout", async (req, p) => { const result = await checkout(env.DB, deps.payments(env), p.id!, await readJson(req), deps); return json({ order: result.order, payment: { provider: "stripe", client_secret: result.client_secret } }, result.created ? 201 : 200); }); // The order id is a random UUID that only the shopper who checked out has. r.on("GET", "/orders/:id", async (_req, p) => { const order = await getOrder(env.DB, p.id!); if (!order) throw new HttpError(404, "order_not_found", "No such order."); return json({ order }); }); return { async fetch(req: Request): Promise<Response> { const url = new URL(req.url); try { const hit = r.match(req.method, url.pathname); if (hit === null) throw new HttpError(404, "not_found", "No such route."); if (hit === "method_not_allowed") throw new HttpError(405, "method_not_allowed", `${req.method} is not allowed here.`); return await hit.handler(req, hit.params); } catch (e) { if (e instanceof HttpError) return errorResponse(e); if (e instanceof URIError) return errorResponse(new HttpError(400, "bad_path", "The path is not valid.")); console.error(JSON.stringify({ msg: "unhandled error", path: url.pathname, error: String(e) })); return errorResponse(new HttpError(500, "internal", "Something went wrong.")); } }, };}