| 1 | import * as D1Client from "@effect/sql-d1/D1Client"; |
| 2 | import type { AnyRelations, EmptyRelations } from "drizzle-orm"; |
| 3 | import type { EffectSQLiteD1Database } from "drizzle-orm/effect-d1"; |
| 4 | import * as SQLiteD1Drizzle from "drizzle-orm/effect-d1"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Layer from "effect/Layer"; |
| 7 | import type { D1DatabaseSource } from "../SQL/D1.ts"; |
| 8 | import { makeExecutionMemo } from "../Runtime/ExecutionMemo.ts"; |
| 9 | import { proxyChain } from "../Util/proxy-chain.ts"; |
| 10 | |
| 11 | /** |
| 12 | * Open a Drizzle database over a Cloudflare D1 binding using the |
| 13 | * `drizzle-orm/effect-d1` integration (which drives queries through |
| 14 | * `@effect/sql-d1`'s `D1Client`). |
| 15 | * |
| 16 | * Accepts the client returned by `Cloudflare.D1.QueryDatabase(db)` — or its |
| 17 | * `raw` effect directly — and returns a chainable Proxy over |
| 18 | * `EffectSQLiteD1Database` (via `proxyChain`): every property read records a |
| 19 | * step, every call records args, and the chain is replayed against the |
| 20 | * resolved drizzle db when it's finally yielded as an Effect. Callers don't |
| 21 | * need a separate `yield* conn` step: |
| 22 | * |
| 23 | * ```typescript |
| 24 | * const d1 = yield* Cloudflare.D1.QueryDatabase(Db); |
| 25 | * const db = yield* Drizzle.D1(d1, { relations }); |
| 26 | * |
| 27 | * fetch: Effect.gen(function* () { |
| 28 | * const rows = yield* db.select().from(users); |
| 29 | * }); |
| 30 | * ``` |
| 31 | * |
| 32 | * The client build is deferred until the first query and memoized on the |
| 33 | * current execution's `Scope` (via {@link makeExecutionMemo}), so the |
| 34 | * `D1Client` (and its prepared-statement cache) is built at most once per |
| 35 | * execution — a Worker `fetch`/`queue`/`scheduled` event, a Durable Object |
| 36 | * call, or a Workflow run — and reused across every query in that execution. |
| 37 | * Resolving the binding is likewise deferred, so deploy / plan-time |
| 38 | * invocations (where `WorkerEnvironment` isn't provided) never touch D1. |
| 39 | * |
| 40 | * The client is built against that same execution scope, so its finalizer |
| 41 | * fires when the scope closes — when the request / run settles, not when the |
| 42 | * Worker's isolate-lifetime init completes. Wrapping queries in a nested |
| 43 | * `Effect.scoped` narrows both the memo and the client's lifetime to that |
| 44 | * block: memo key and finalizer target are always the same scope object, so |
| 45 | * they cannot disagree. |
| 46 | * |
| 47 | * @binding |
| 48 | */ |
| 49 | export const D1 = < |
| 50 | TRelations extends AnyRelations = EmptyRelations, |
| 51 | E = never, |
| 52 | R = never, |
| 53 | >( |
| 54 | database: D1DatabaseSource<E, R>, |
| 55 | config?: SQLiteD1Drizzle.EffectDrizzleSQLiteD1Config<TRelations>, |
| 56 | ) => |
| 57 | Effect.map( |
| 58 | makeExecutionMemo( |
| 59 | Effect.gen(function* () { |
| 60 | const db = yield* Effect.isEffect(database) ? database : database.raw; |
| 61 | const d1Ctx = yield* Layer.build(D1Client.layer({ db })); |
| 62 | return yield* SQLiteD1Drizzle.makeWithDefaults( |
| 63 | config ?? |
| 64 | ({} as SQLiteD1Drizzle.EffectDrizzleSQLiteD1Config<TRelations>), |
| 65 | ).pipe(Effect.provideContext(d1Ctx)); |
| 66 | }), |
| 67 | ), |
| 68 | (db) => |
| 69 | proxyChain< |
| 70 | EffectSQLiteD1Database<TRelations> & { |
| 71 | $client: D1Client.D1Client; |
| 72 | } |
| 73 | >( |
| 74 | db as Effect.Effect< |
| 75 | EffectSQLiteD1Database<TRelations> & { |
| 76 | $client: D1Client.D1Client; |
| 77 | } |
| 78 | >, |
| 79 | ), |
| 80 | ); |