| 1 | import type * as runtime from "@cloudflare/workers-types"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Binding from "../../Binding.ts"; |
| 4 | import type { RuntimeContext } from "../../RuntimeContext.ts"; |
| 5 | import type { Database } from "./Database.ts"; |
| 6 | |
| 7 | /** |
| 8 | * Bind a {@link Database} to a Worker and obtain the Effect-native D1 |
| 9 | * connection client (`prepare`, `exec`, `batch`, `raw`). |
| 10 | * |
| 11 | * `QueryDatabase` is a single identifier that is simultaneously the binding's |
| 12 | * Context tag, its type, and the callable — |
| 13 | * `yield* Cloudflare.D1.QueryDatabase(db)`. |
| 14 | * |
| 15 | * **Example:** Querying a database inside a Worker |
| 16 | * ```typescript |
| 17 | * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); |
| 18 | * const rows = yield* db.prepare("SELECT * FROM users").all(); |
| 19 | * ``` |
| 20 | * |
| 21 | * @binding |
| 22 | * @product D1 |
| 23 | * @category Storage & Databases |
| 24 | */ |
| 25 | export interface QueryDatabase extends Binding.Service< |
| 26 | QueryDatabase, |
| 27 | "Cloudflare.D1.QueryDatabase", |
| 28 | (database: Database) => Effect.Effect<QueryDatabaseClient> |
| 29 | > {} |
| 30 | |
| 31 | export const QueryDatabase = Binding.Service<QueryDatabase>( |
| 32 | "Cloudflare.D1.QueryDatabase", |
| 33 | ); |
| 34 | |
| 35 | /** |
| 36 | * Effect-native wrapper around a Cloudflare D1 prepared statement. |
| 37 | * |
| 38 | * Construction (`prepare`) and binding (`bind`) are synchronous — |
| 39 | * they're plan builders and don't touch D1. Only the executors |
| 40 | * (`all`, `first`, `run`, `raw`) round-trip to the database, and |
| 41 | * those return Effects so they participate in the Effect runtime. |
| 42 | */ |
| 43 | export class PreparedStatement { |
| 44 | /** @internal */ |
| 45 | constructor( |
| 46 | private readonly query: string, |
| 47 | private readonly binds: ReadonlyArray<unknown>, |
| 48 | private readonly rawEff: Effect.Effect<runtime.D1Database>, |
| 49 | ) {} |
| 50 | |
| 51 | /** |
| 52 | * Return a new prepared statement bound to `values`. Subsequent |
| 53 | * calls replace the previous binding, matching the underlying |
| 54 | * Cloudflare runtime semantics. |
| 55 | */ |
| 56 | bind(...values: unknown[]): PreparedStatement { |
| 57 | return new PreparedStatement(this.query, values, this.rawEff); |
| 58 | } |
| 59 | |
| 60 | /** Run the query and return all matching rows. */ |
| 61 | all<T = unknown>(): Effect.Effect< |
| 62 | runtime.D1Result<T>, |
| 63 | never, |
| 64 | RuntimeContext |
| 65 | > { |
| 66 | return this.withRuntime((stmt) => stmt.all<T>()); |
| 67 | } |
| 68 | |
| 69 | /** Run the query and return the first row, or `null` if no rows. */ |
| 70 | first<T = unknown>(): Effect.Effect<T | null, never, RuntimeContext>; |
| 71 | first<T = unknown>( |
| 72 | column: string, |
| 73 | ): Effect.Effect<T | null, never, RuntimeContext>; |
| 74 | first<T = unknown>( |
| 75 | column?: string, |
| 76 | ): Effect.Effect<T | null, never, RuntimeContext> { |
| 77 | return this.withRuntime((stmt) => |
| 78 | column !== undefined ? stmt.first<T>(column) : stmt.first<T>(), |
| 79 | ); |
| 80 | } |
| 81 | |
| 82 | /** Run the query as a mutation; returns row metadata. */ |
| 83 | run<T = unknown>(): Effect.Effect< |
| 84 | runtime.D1Result<T>, |
| 85 | never, |
| 86 | RuntimeContext |
| 87 | > { |
| 88 | return this.withRuntime((stmt) => stmt.run<T>()); |
| 89 | } |
| 90 | |
| 91 | /** Run the query and return rows as flat arrays. */ |
| 92 | raw<T = unknown[]>(): Effect.Effect<T[], never, RuntimeContext>; |
| 93 | raw<T = unknown[]>(options: { |
| 94 | columnNames: true; |
| 95 | }): Effect.Effect<[string[], ...T[]], never, RuntimeContext>; |
| 96 | raw<T = unknown[]>(options?: { |
| 97 | columnNames: true; |
| 98 | }): Effect.Effect<T[] | [string[], ...T[]], never, RuntimeContext> { |
| 99 | return this.withRuntime((stmt) => |
| 100 | options |
| 101 | ? stmt.raw<T>(options) |
| 102 | : (stmt.raw<T>() as Promise<T[] | [string[], ...T[]]>), |
| 103 | ); |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * Materialize the underlying Cloudflare prepared statement against |
| 108 | * a concrete D1 binding. Used by `QueryDatabaseClient.batch` to |
| 109 | * collect statements for a single transactional call. |
| 110 | * |
| 111 | * @internal |
| 112 | */ |
| 113 | _build(raw: runtime.D1Database): runtime.D1PreparedStatement { |
| 114 | const stmt = raw.prepare(this.query); |
| 115 | return this.binds.length > 0 ? stmt.bind(...this.binds) : stmt; |
| 116 | } |
| 117 | |
| 118 | private withRuntime<A>( |
| 119 | fn: (stmt: runtime.D1PreparedStatement) => Promise<A>, |
| 120 | ): Effect.Effect<A, never, RuntimeContext> { |
| 121 | return Effect.flatMap(this.rawEff, (raw) => |
| 122 | Effect.promise(() => fn(this._build(raw))), |
| 123 | ) as Effect.Effect<A, never, RuntimeContext>; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | export interface QueryDatabaseClient { |
| 128 | /** |
| 129 | * An Effect that resolves to the raw underlying Cloudflare D1Database binding. |
| 130 | * Use this when you need direct access for libraries like Better Auth. |
| 131 | */ |
| 132 | raw: Effect.Effect<runtime.D1Database, never, RuntimeContext>; |
| 133 | /** |
| 134 | * Prepare a SQL statement. Returns synchronously — the network |
| 135 | * round-trip happens when you yield one of the statement's |
| 136 | * executors (`all`, `first`, `run`, `raw`). |
| 137 | */ |
| 138 | prepare: (query: string) => PreparedStatement; |
| 139 | /** |
| 140 | * Execute raw SQL without prepared statements. |
| 141 | */ |
| 142 | exec: ( |
| 143 | query: string, |
| 144 | ) => Effect.Effect<runtime.D1ExecResult, never, RuntimeContext>; |
| 145 | /** |
| 146 | * Send multiple prepared statements in a single call. |
| 147 | * Statements execute sequentially and are rolled back on failure. |
| 148 | */ |
| 149 | batch: <T = unknown>( |
| 150 | statements: PreparedStatement[], |
| 151 | ) => Effect.Effect<runtime.D1Result<T>[], never, RuntimeContext>; |
| 152 | } |