38 lines · 1.7 KB
import { accountDb, repoDb } from "./scoped-db";
import type { ScopedValue } from "./scoped-db";
import { devMailMode } from "./notify/dev-mail";

export const DEV_SCOPED_SQL_PATH = "/__dev/scoped-sql";
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);

type Body = { scope: "repo" | "account"; id: string; sql: string; params?: ScopedValue[] };

function isBody(value: unknown): value is Body {
  if (typeof value !== "object" || value === null) return false;
  const body = value as Record<string, unknown>;
  return (
    (body.scope === "repo" || body.scope === "account") &&
    typeof body.id === "string" &&
    typeof body.sql === "string" &&
    (body.params === undefined || Array.isArray(body.params))
  );
}

/**
 * E2E harness access to a RepoState / AccountState database, the scoped counterpart of `wrangler d1 execute`
 * (those tables are no longer in D1). Same gate as the dev mail sink: 404 unless EMAIL_DEV_SINK=kv, PUBLIC_URL
 * is loopback, and the request targets a loopback host. POST `{ scope, id, sql, params }`, returns the rows.
 */
export async function handleDevScopedSql(request: Request, url: URL): Promise<Response | null> {
  if (url.pathname !== DEV_SCOPED_SQL_PATH) return null;
  const host = url.hostname.toLowerCase();
  const loopback = LOCAL_HOSTS.has(host) || host.endsWith(".localhost");
  if (devMailMode() !== "kv" || !loopback || request.method !== "POST") {
    return new Response("Not found\n", { status: 404 });
  }
  const body: unknown = await request.json().catch(() => null);
  if (!isBody(body)) return new Response("Bad request\n", { status: 400 });
  const db = body.scope === "repo" ? repoDb(body.id) : accountDb(body.id);
  return Response.json(await db.all(body.sql, ...(body.params ?? [])));
}