import type * as runtime from "@cloudflare/workers-types"; import * as d1 from "@distilled.cloud/cloudflare/d1"; import * as Effect from "effect/Effect"; import type * as HttpClient from "effect/unstable/http/HttpClient"; import type { Credentials } from "../Credentials.ts"; import { type QueryDatabaseClient, PreparedStatement, } from "./QueryDatabase.ts"; // Shared HTTP scaffolding for the D1 `QueryDatabase` binding. NOT re-exported // from `index.ts` — only the contract and the impl layers are public. A future // token-scoped `QueryDatabaseHttp` layer reuses this builder with an auth // minted from an `AccountApiToken`; `QueryDatabaseLocal` builds the auth from // the ambient current-credentials context. // // PreparedStatement (shared with QueryDatabaseBinding) drives a // `runtime.D1Database` whose executors return Promises. The shim below // implements the slice PreparedStatement uses (prepare/exec/batch + stmt // bind/all/first/run/raw) by running `d1.queryDatabase` over HTTP. /** * Injectable auth shared by the Local (current-credentials) impl and a future * Http (scoped-token) impl. `authorize` discharges the * `Credentials | HttpClient` requirement of a distilled op; `accountId` is the * Cloudflare account the ops run against. */ export interface D1Auth { authorize: ( eff: Effect.Effect, ) => Effect.Effect; accountId: string; } /** * Build a {@link QueryDatabaseClient} over the D1 HTTP query API. * * `databaseId` is an Effect so the resolution stays deferred to each call — * inside an Action it resolves through the apply-time RuntimeContext. The * credentials are provided ONLY around the distilled op (via `auth.authorize`), * never around the id accessor, matching the KV / Vectorize Local variants. */ export const makeHttpQueryDatabaseClient = ( auth: D1Auth, databaseId: Effect.Effect, ): QueryDatabaseClient => makeQueryDatabaseClientFrom( Effect.map(databaseId, (id) => makeHttpD1Database(auth, id)), ); /** * Build a {@link QueryDatabaseClient} from a deferred `D1Database` — shared * by the cloud HTTP transport above and the local-simulator gateway * transport (`QueryDatabaseLocal` with a `dev:` id). */ export const makeQueryDatabaseClientFrom = ( rawEff: Effect.Effect, ): QueryDatabaseClient => { return { raw: rawEff, prepare: (query: string) => new PreparedStatement(query, [], rawEff), exec: (query: string) => Effect.flatMap(rawEff, (raw) => Effect.promise(() => raw.exec(query))), batch: (statements: PreparedStatement[]) => Effect.flatMap(rawEff, (raw) => Effect.promise(() => raw.batch(statements.map((s) => s._build(raw))), ), ), } satisfies QueryDatabaseClient; }; const runQuery = ( auth: D1Auth, databaseId: string, body: | { sql: string; params?: unknown[] } | { batch: { sql: string; params?: unknown[] }[] }, ): Promise => auth .authorize( d1.queryDatabase({ accountId: auth.accountId, databaseId, ...(body as any), }), ) .pipe(Effect.runPromise); const toResult = ( r: | { results?: unknown; success?: boolean | null; meta?: unknown } | undefined, ): runtime.D1Result => ({ results: (r?.results ?? []) as T[], success: r?.success ?? true, meta: (r?.meta ?? {}) as any, }) as runtime.D1Result; /** * Normalize a bound value the way the native D1 binding does before it reaches * SQLite. Over the raw HTTP query API, unlike the Worker binding, values are * bound verbatim — a JS `true` would arrive as the string `"true"`. Match the * native semantics: booleans become integers (1/0) and binary becomes a byte * array (BLOB). `null`, numbers, and strings pass through unchanged. */ const normalizeBind = (value: unknown): unknown => { if (typeof value === "boolean") return value ? 1 : 0; if (value instanceof ArrayBuffer) { return Array.from(new Uint8Array(value)); } if (ArrayBuffer.isView(value)) { return Array.from( new Uint8Array(value.buffer, value.byteOffset, value.byteLength), ); } return value; }; /** A `D1Database` facade over the cloud HTTP query API. */ export const makeHttpD1Database = ( auth: D1Auth, databaseId: string, ): runtime.D1Database => makeD1DatabaseFromTransport((body) => runQuery(auth, databaseId, body)); /** * The transport a {@link runtime.D1Database} facade is built over: run one * query (or batch) and return the D1 result envelope(s). Two transports * exist — the cloud HTTP API (`runQuery`) and the local-simulator gateway * (`withLocalD1Query`). */ export type D1QueryTransport = ( body: | { sql: string; params?: unknown[] } | { batch: { sql: string; params?: unknown[] }[] }, ) => Promise<{ result: Array<{ results?: unknown; success?: boolean | null; meta?: unknown; }>; }>; export const makeD1DatabaseFromTransport = ( transport: D1QueryTransport, ): runtime.D1Database => { const makeStatement = ( query: string, binds: ReadonlyArray, ): runtime.D1PreparedStatement => { const exec = async () => { const res = await transport({ sql: query, params: binds.length ? binds.map(normalizeBind) : undefined, }); return res.result[0]; }; return { bind: (...values: unknown[]) => makeStatement(query, values), first: (async (column?: string) => { const first = ( (await exec())?.results as Record[] | undefined )?.[0]; if (first == null) return null; return column !== undefined ? (first[column] ?? null) : first; }) as runtime.D1PreparedStatement["first"], all: (async () => toResult(await exec())) as runtime.D1PreparedStatement["all"], run: (async () => toResult(await exec())) as runtime.D1PreparedStatement["run"], raw: (async (options?: { columnNames?: boolean }) => { const rows = ((await exec())?.results ?? []) as Record< string, unknown >[]; const arrays = rows.map((row) => Object.values(row)); if (options?.columnNames && rows[0]) { return [Object.keys(rows[0]), ...arrays]; } return arrays; }) as runtime.D1PreparedStatement["raw"], // Carry query + params so `batch` can reconstruct the request. __query: query, __params: binds, } as unknown as runtime.D1PreparedStatement; }; return { prepare: (query: string) => makeStatement(query, []), exec: async (query: string) => { const res = await transport({ sql: query }); const meta = res.result[res.result.length - 1]?.meta as | { duration?: number } | undefined; return { count: res.result.length, duration: meta?.duration ?? 0, } as runtime.D1ExecResult; }, batch: async (statements: runtime.D1PreparedStatement[]) => { const res = await transport({ batch: statements.map((s) => ({ sql: (s as any).__query as string, params: ((s as any).__params as unknown[]).length ? ((s as any).__params as unknown[]).map(normalizeBind) : undefined, })), }); return res.result.map((r) => toResult(r)); }, } as unknown as runtime.D1Database; };