import type * as runtime from "@cloudflare/workers-types"; import * as Effect from "effect/Effect"; import * as Binding from "../../Binding.ts"; import type { RuntimeContext } from "../../RuntimeContext.ts"; import type { Database } from "./Database.ts"; /** * Bind a {@link Database} to a Worker and obtain the Effect-native D1 * connection client (`prepare`, `exec`, `batch`, `raw`). * * `QueryDatabase` is a single identifier that is simultaneously the binding's * Context tag, its type, and the callable — * `yield* Cloudflare.D1.QueryDatabase(db)`. * * **Example:** Querying a database inside a Worker * ```typescript * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); * const rows = yield* db.prepare("SELECT * FROM users").all(); * ``` * * @binding * @product D1 * @category Storage & Databases */ export interface QueryDatabase extends Binding.Service< QueryDatabase, "Cloudflare.D1.QueryDatabase", (database: Database) => Effect.Effect > {} export const QueryDatabase = Binding.Service( "Cloudflare.D1.QueryDatabase", ); /** * Effect-native wrapper around a Cloudflare D1 prepared statement. * * Construction (`prepare`) and binding (`bind`) are synchronous — * they're plan builders and don't touch D1. Only the executors * (`all`, `first`, `run`, `raw`) round-trip to the database, and * those return Effects so they participate in the Effect runtime. */ export class PreparedStatement { /** @internal */ constructor( private readonly query: string, private readonly binds: ReadonlyArray, private readonly rawEff: Effect.Effect, ) {} /** * Return a new prepared statement bound to `values`. Subsequent * calls replace the previous binding, matching the underlying * Cloudflare runtime semantics. */ bind(...values: unknown[]): PreparedStatement { return new PreparedStatement(this.query, values, this.rawEff); } /** Run the query and return all matching rows. */ all(): Effect.Effect< runtime.D1Result, never, RuntimeContext > { return this.withRuntime((stmt) => stmt.all()); } /** Run the query and return the first row, or `null` if no rows. */ first(): Effect.Effect; first( column: string, ): Effect.Effect; first( column?: string, ): Effect.Effect { return this.withRuntime((stmt) => column !== undefined ? stmt.first(column) : stmt.first(), ); } /** Run the query as a mutation; returns row metadata. */ run(): Effect.Effect< runtime.D1Result, never, RuntimeContext > { return this.withRuntime((stmt) => stmt.run()); } /** Run the query and return rows as flat arrays. */ raw(): Effect.Effect; raw(options: { columnNames: true; }): Effect.Effect<[string[], ...T[]], never, RuntimeContext>; raw(options?: { columnNames: true; }): Effect.Effect { return this.withRuntime((stmt) => options ? stmt.raw(options) : (stmt.raw() as Promise), ); } /** * Materialize the underlying Cloudflare prepared statement against * a concrete D1 binding. Used by `QueryDatabaseClient.batch` to * collect statements for a single transactional call. * * @internal */ _build(raw: runtime.D1Database): runtime.D1PreparedStatement { const stmt = raw.prepare(this.query); return this.binds.length > 0 ? stmt.bind(...this.binds) : stmt; } private withRuntime( fn: (stmt: runtime.D1PreparedStatement) => Promise, ): Effect.Effect { return Effect.flatMap(this.rawEff, (raw) => Effect.promise(() => fn(this._build(raw))), ) as Effect.Effect; } } export interface QueryDatabaseClient { /** * An Effect that resolves to the raw underlying Cloudflare D1Database binding. * Use this when you need direct access for libraries like Better Auth. */ raw: Effect.Effect; /** * Prepare a SQL statement. Returns synchronously — the network * round-trip happens when you yield one of the statement's * executors (`all`, `first`, `run`, `raw`). */ prepare: (query: string) => PreparedStatement; /** * Execute raw SQL without prepared statements. */ exec: ( query: string, ) => Effect.Effect; /** * Send multiple prepared statements in a single call. * Statements execute sequentially and are rolled back on failure. */ batch: ( statements: PreparedStatement[], ) => Effect.Effect[], never, RuntimeContext>; }