Menu

Examples

Commerce

A full Kovo storefront — product grid, cart badge, and order history — running live next to the authored components, queries, and derived optimism that drive it.

examples/commerce/src/components/product-grid.tsxtsx
/** @jsxImportSource @kovojs/server */
import { component, FieldError, form, FormError } from '@kovojs/core';
import { Badge } from '@kovojs/ui/badge';
import { Button } from '@kovojs/ui/button';
import { Card } from '@kovojs/ui/card';
import { tokens } from '@kovojs/style';
import * as style from '@kovojs/style';

import { addToCart, type ProductGridResult } from '../domain.js';
import { productGridQuery } from '../queries.js';

const addToCartForm = form('cart/add');

const productGridStyles = style.create({
  errorText: {
    color: tokens.sys.color.error,
    fontSize: 14,
  },
  field: {
    backgroundColor: tokens.sys.color.surfaceContainerLowest,
    borderColor: tokens.sys.color.outline,
    borderRadius: tokens.sys.shape.cornerMedium,
    borderStyle: 'solid',
    borderWidth: 1,
    boxSizing: 'border-box',
    color: tokens.sys.color.onSurface,
    paddingBlock: 6,
    paddingInline: 10,
  },
  formLabel: {
    color: tokens.sys.color.onSurfaceVariant,
    display: 'grid',
    fontSize: 12,
    fontWeight: 500,
    gap: 4,
  },
  link: {
    color: tokens.sys.color.primary,
    fontSize: 14,
    fontWeight: 500,
    textDecoration: 'none',
  },
  panelError: {
    backgroundColor: tokens.sys.color.errorContainer,
    borderColor: tokens.sys.color.error,
    borderRadius: tokens.sys.shape.cornerMedium,
    borderStyle: 'solid',
    borderWidth: 1,
    color: tokens.sys.color.onErrorContainer,
    fontSize: 14,
    padding: 16,
  },
  productEmoji: {
    backgroundColor: tokens.sys.color.surfaceContainer,
    borderRadius: tokens.sys.shape.cornerMedium,
    display: 'grid',
    fontSize: 24,
    height: 48,
    placeItems: 'center',
    width: 48,
  },
  productForm: {
    alignItems: 'end',
    display: 'flex',
    flexWrap: 'wrap',
    gap: 8,
  },
  row: {
    alignItems: 'center',
    display: 'flex',
    gap: 16,
  },
  rowBetween: {
    alignItems: 'center',
    display: 'flex',
    justifyContent: 'space-between',
  },
  stack: {
    display: 'grid',
    gap: 16,
  },
  stackSm: {
    display: 'grid',
    gap: 4,
  },
  tabularStrong: {
    fontVariantNumeric: 'tabular-nums',
    fontWeight: 600,
  },
  title: {
    color: tokens.sys.color.onSurface,
    fontWeight: 600,
    letterSpacing: 0,
    margin: 0,
  },
});

export const productGridStyleCss = style.emitAtomicCss(
  Object.values(productGridStyles).flatMap((entry) => entry.__rules ?? []),
);

export interface OutOfStockFailure {
  code: 'OUT_OF_STOCK';
  payload: { availableQuantity: number };
}

export const ProductGrid = component({
  errorBoundary: {
    fallback: renderProductGridError,
    target: 'product-grid',
  },
  mutations: { addToCart: addToCartForm },
  queries: { productGrid: productGridQuery },
  render: ({ productGrid }: { productGrid: ProductGridResult }) => {
    const { nextCursor } = productGrid;
    return (
      <section data-page-cursor={nextCursor ?? ''}>{renderProductGridItems(productGrid)}</section>
    );
  },
});

export function ProductGridError(): string {
  return renderProductGridError();
}

function renderProductGridError(): string {
  return (
    <section style={productGridStyles.panelError}>Products are temporarily unavailable.</section>
  );
}

export function renderProductGridItems(result: ProductGridResult): string {
  const cards = result.items.map((item) => renderProductCard(item));
  const cursor = result.nextCursor;
  return (
    <>
      {cards}
      {cursor ? (
        <a style={productGridStyles.link} href={`/products?after=${cursor}`} data-cursor={cursor}>
          More
        </a>
      ) : (
        ''
      )}
    </>
  );
}

export interface ProductItem {
  id: string;
  name: string;
  category: string;
  emoji: string;
  stock: number;
  unitPrice: number;
}

/** Format an integer cent amount as `$25.99`. */
export function priceLabel(cents: number): string {
  return `$${(cents / 100).toFixed(2)}`;
}

/** Low stock reads as a warning badge; healthy stock as success. */
function stockBadge(stock: number): string {
  if (stock === 0) return Badge.definition.render({ variant: 'warning', children: 'Sold out' });
  if (stock <= 2)
    return Badge.definition.render({ variant: 'warning', children: `Only ${stock} left` });
  return Badge.definition.render({ variant: 'success', children: `${stock} in stock` });
}

function renderProductCard(item: ProductItem): string {
  const body = (
    <div style={productGridStyles.stack}>
      <div style={productGridStyles.row}>
        <span style={productGridStyles.productEmoji}>{item.emoji}</span>
        <div style={productGridStyles.stackSm}>
          <h2 style={productGridStyles.title}>{item.name}</h2>
          {Badge.definition.render({ variant: 'neutral', children: item.category })}
        </div>
      </div>
      <div style={productGridStyles.rowBetween}>
        <span style={productGridStyles.tabularStrong}>{priceLabel(item.unitPrice)}</span>
        {stockBadge(item.stock)}
      </div>
      {renderAddToCartForm(item)}
    </div>
  );
  return <article kovo-key={item.id}>{Card.definition.render({ children: body })}</article>;
}

export function renderAddToCartForm(item: { id: string; stock: number }): string {
  const soldOut = item.stock === 0;
  return (
    <form enhance mutation={addToCart} key={item.id} style={productGridStyles.productForm}>
      <input type="hidden" name="productId" value={item.id} />
      <label style={productGridStyles.formLabel}>
        <span>Qty</span>
        <input
          style={productGridStyles.field}
          name="quantity"
          type="number"
          min="1"
          max={item.stock}
          value="1"
        />
        <FieldError name="quantity" style={productGridStyles.errorText} />
      </label>
      {Button.definition.render({
        children: soldOut ? 'Sold out' : 'Add to cart',
        disabled: soldOut,
        type: 'submit',
        variant: 'primary',
      })}
      <FormError
        code="OUT_OF_STOCK"
        style={productGridStyles.errorText}
        message={(failure: OutOfStockFailure) =>
          `Only ${failure.payload.availableQuantity} available.`
        }
      />
    </form>
  );
}
examples/commerce/src/components/cart-badge.tsxtsx
/** @jsxImportSource @kovojs/server */
import { component } from '@kovojs/core';
import { t } from '@kovojs/server';
import { tokens } from '@kovojs/style';
import * as style from '@kovojs/style';

import { commerceMessages, type CartQueryResult } from '../domain.js';
import { cartQuery } from '../queries.js';

const cartBadgeStyles = style.create({
  badge: {
    alignItems: 'center',
    backgroundColor: tokens.sys.color.surfaceContainerLowest,
    borderColor: tokens.sys.color.outlineVariant,
    borderRadius: tokens.sys.shape.cornerMedium,
    borderStyle: 'solid',
    borderWidth: 1,
    color: tokens.sys.color.onSurface,
    display: 'inline-flex',
    fontSize: 14,
    fontWeight: 500,
    gap: 8,
    paddingBlock: 8,
    paddingInline: 12,
  },
  count: {
    alignItems: 'center',
    backgroundColor: tokens.sys.color.primary,
    borderRadius: tokens.sys.shape.cornerFull,
    color: tokens.sys.color.onPrimary,
    display: 'inline-flex',
    fontSize: 12,
    fontVariantNumeric: 'tabular-nums',
    fontWeight: 600,
    height: 20,
    justifyContent: 'center',
    minWidth: 20,
    paddingInline: 6,
  },
});

export const cartBadgeStyleCss = style.emitAtomicCss(
  Object.values(cartBadgeStyles).flatMap((entry) => entry.__rules ?? []),
);

export const CartBadge = component({
  queries: { cart: cartQuery },
  render: ({ cart }: { cart: CartQueryResult }) => (
    <cart-badge style={cartBadgeStyles.badge}>
      <span>{t(commerceMessages, 'cartLabel')}</span>
      <span style={cartBadgeStyles.count}>{cart.count}</span>
    </cart-badge>
  ),
});
examples/commerce/src/components/order-history.tsxtsx
/** @jsxImportSource @kovojs/server */
import { component } from '@kovojs/core';
import { Badge } from '@kovojs/ui/badge';
import { tokens } from '@kovojs/style';
import * as style from '@kovojs/style';

import type { OrderHistoryResult } from '../domain.js';
import { orderHistoryQuery } from '../queries.js';
import { priceLabel } from './product-grid.js';

const orderHistoryStyles = style.create({
  item: {
    alignItems: 'center',
    backgroundColor: tokens.sys.color.surfaceContainerLowest,
    borderColor: tokens.sys.color.outlineVariant,
    borderRadius: tokens.sys.shape.cornerMedium,
    borderStyle: 'solid',
    borderWidth: 1,
    display: 'flex',
    justifyContent: 'space-between',
    paddingBlock: 12,
    paddingInline: 16,
  },
  mutedText: {
    color: tokens.sys.color.onSurfaceVariant,
    fontSize: 12,
  },
  row: {
    alignItems: 'center',
    display: 'flex',
    gap: 16,
  },
  stack: {
    display: 'grid',
    gap: 16,
  },
  stackSm: {
    display: 'grid',
    gap: 4,
  },
  tabularStrong: {
    fontVariantNumeric: 'tabular-nums',
    fontWeight: 600,
  },
  title: {
    color: tokens.sys.color.onSurface,
    fontWeight: 600,
    letterSpacing: 0,
    margin: 0,
  },
});

export const orderHistoryStyleCss = style.emitAtomicCss(
  Object.values(orderHistoryStyles).flatMap((entry) => entry.__rules ?? []),
);

export const OrderHistory = component({
  queries: { orderHistory: orderHistoryQuery },
  render: ({ orderHistory }: { orderHistory: OrderHistoryResult }) => (
    <ol style={orderHistoryStyles.stack}>{renderOrderHistoryItems(orderHistory)}</ol>
  ),
});

interface OrderHistoryItem {
  id: string;
  productId: string;
  qty: number;
  total: number;
}

export function renderOrderHistoryItems(result: OrderHistoryResult): string {
  return (
    <>
      {result.items.map((item: OrderHistoryItem) => (
        <li kovo-key={item.id} style={orderHistoryStyles.item}>
          <div style={orderHistoryStyles.stackSm}>
            <span style={orderHistoryStyles.title}>{item.productId}</span>
            <span style={orderHistoryStyles.mutedText}>Order {item.id}</span>
          </div>
          <div style={orderHistoryStyles.row}>
            {Badge.definition.render({ children: `×${item.qty}`, variant: 'neutral' })}
            <span style={orderHistoryStyles.tabularStrong}>{priceLabel(item.total)}</span>
          </div>
        </li>
      ))}
    </>
  );
}
examples/commerce/src/queries.tsts
import { guards, query, type QueryLoadContext } from '@kovojs/server';
import { eq, gt, sum } from 'drizzle-orm';
import { domain } from '@kovojs/server';

import type { CommerceDb } from './db.js';
import { cartItems, orders, products } from './schema.js';

export interface CartQueryResult {
  count: number;
}

export interface ProductGridInput {
  after?: string;
  limit?: number;
}

export interface ProductGridResult {
  items: {
    id: string;
    name: string;
    category: string;
    emoji: string;
    stock: number;
    unitPrice: number;
  }[];
  nextCursor: string | null;
}

export interface OrderHistoryResult {
  items: { id: string; productId: string; qty: number; total: number; userId: string }[];
}

export interface CommerceQueryRequest {
  db: CommerceDb;
  // SECURITY (SECURITY_FINDINGS.md M9): order-history reads are per-user, so the
  // query request must be able to carry the authenticated session whose user id
  // scopes the rows. Cart/product reads remain global (no session needed).
  session?: { id?: string; user?: { id?: string } | null } | null;
}

export const cart = domain('cart');
export const order = domain('order');
export const product = domain('product');

type CommerceQueryLoadContext = QueryLoadContext<CommerceQueryRequest> & {
  db?: CommerceDb;
  session?: CommerceQueryRequest['session'];
};

export const cartQuery = query('cart', {
  async load(_input: unknown, context?: CommerceQueryLoadContext): Promise<CartQueryResult> {
    const db = requireCommerceQueryDb(context);
    const rows = await db.select({ value: sum(cartItems.qty) }).from(cartItems);
    return { count: Number(rows[0]?.value ?? 0) };
  },
  reads: [cart],
});

export const productGridQuery = query('productGrid', {
  async load(input: unknown, context?: CommerceQueryLoadContext): Promise<ProductGridResult> {
    const db = requireCommerceQueryDb(context);
    const { after, limit } = (input ?? {}) as ProductGridInput;
    const pageSize = limit ?? 2;
    const items = await db
      .select({
        id: products.id,
        name: products.name,
        category: products.category,
        emoji: products.emoji,
        stock: products.stock,
        unitPrice: products.unitPrice,
      })
      .from(products)
      .where(after ? gt(products.id, after) : undefined)
      .orderBy(products.id)
      .limit(pageSize);
    const last = items.at(-1);
    const more = last
      ? await db.select({ id: products.id }).from(products).where(gt(products.id, last.id)).limit(1)
      : [];
    const nextCursor = more.length > 0 ? (last?.id ?? null) : null;
    return { items: items, nextCursor: nextCursor };
  },
  reads: [product],
});

export const orderHistoryQuery = query('orderHistory', {
  // SECURITY (SECURITY_FINDINGS.md M9): order history is per-user, so this read must
  // require an authenticated session — the endpoint guard rejects unauthenticated
  // callers, and the `load` below additionally scopes the rowset to that user's id
  // so no caller can ever observe another user's orders.
  guard: guards.authed<CommerceQueryRequest>(),
  async load(_input: unknown, context?: CommerceQueryLoadContext): Promise<OrderHistoryResult> {
    const db = requireCommerceQueryDb(context);
    const userId = requireCommerceQueryUserId(context);
    // Orders are an append-only log. The user filter keeps the rowset scoped to
    // the authenticated session.
    const items = await db
      .select({
        id: orders.id,
        productId: orders.productId,
        qty: orders.qty,
        total: orders.total,
        userId: orders.userId,
      })
      .from(orders)
      .where(eq(orders.userId, userId));
    return { items: items };
  },
  // SPEC §9.1.1: the `items` collection is keyed by order `id` and scoped by the
  // `order` domain, so an `order`-touching mutation that carries the changed
  // order id ships only the new order row instead of the whole history.
  // (Compiler-derived delta meta is the deferred zero-config piece; this
  // declares it explicitly today.)
  delta: [{ domain: 'order', key: 'id', path: 'items' }],
  reads: [order],
});

export async function loadCartQuery(db: CommerceDb): Promise<CartQueryResult> {
  return cartQuery.load(undefined, { db, request: { db } });
}

export async function loadProductGrid(
  db: CommerceDb,
  input: ProductGridInput = {},
): Promise<ProductGridResult> {
  return productGridQuery.load(input, { db, request: { db } });
}

// SECURITY (SECURITY_FINDINGS.md M9): the order-history loader now requires the
// authenticated user id so it can scope the read; callers thread the session user
// id from the request.
export async function loadOrderHistory(
  db: CommerceDb,
  userId: string,
): Promise<OrderHistoryResult> {
  const session = { id: userId, user: { id: userId } };
  return orderHistoryQuery.load(undefined, { db, request: { db, session }, session });
}

function requireCommerceQueryDb(context?: CommerceQueryLoadContext): CommerceDb {
  const db = context?.db ?? context?.request?.db;

  if (!db) {
    throw new Error('commerce query loaders require context.db or request.db');
  }

  return db;
}

function requireCommerceQueryUserId(context?: CommerceQueryLoadContext): string {
  const userId = context?.session?.user?.id ?? context?.request?.session?.user?.id;

  if (!userId) {
    // Default-deny: order history is per-user and must never fall back to an
    // unscoped read. A missing user id means the caller is unauthenticated.
    throw new Error('orderHistory query requires an authenticated session user id');
  }

  return userId;
}
examples/commerce/src/generated/optimistic/cart-add.tsts
import '../live-targets.js';

// DO NOT EDIT — generated by @kovojs/drizzle derived optimism (SPEC.md §10.5).
// Override a transform by declaring it in the mutation module; deleting the
// override lets derivation take this pair back over, pair by pair (SPEC.md §10.4).
import { tempId, type OptimisticFor } from '@kovojs/runtime';

import type { addToCartForm } from '../../domain.js';

export const cartAddDerivedOptimistic = {
  queue: 'cart',
  transforms: {
    cart: (current, $input) => {
      const next = structuredClone(current);
      next.count = (next.count ?? 0) + $input.quantity;
      return next;
    },
    orderHistory: (current, $input) => {
      const next = structuredClone(current);
      next.items.push({ id: tempId(), productId: $input.productId, qty: $input.quantity, total: 0, userId: tempId() });
      return next;
    },
    productGrid: (current, $input) => {
      const next = structuredClone(current);
      {
        const target = next.items.find((entry) => entry.id === $input.productId);
        if (target) {
          target.stock = (target.stock - $input.quantity);
        }
      }
      return next;
    },
  },
} satisfies OptimisticFor<typeof addToCartForm>;