1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import type * as HttpClient from "effect/http/HttpClient";
4import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
5import type { Credentials } from "../Credentials.ts";
6import { isLocalId } from "../LocalRuntime.ts";
7import type { Database } from "./Database.ts";
8import { localD1GatewayRuntime, withLocalD1Query } from "./LocalD1Gateway.ts";
9import { QueryDatabase } from "./QueryDatabase.ts";
10import {
11 makeD1DatabaseFromTransport,
12 makeHttpD1Database,
13 makeQueryDatabaseClientFrom,
14} from "./QueryDatabaseHttpClient.ts";
15
16/**
17 * Local implementation of the {@link QueryDatabase} binding — queries D1 over
18 * the Cloudflare HTTP API using the **current credentials** instead of a native
19 * Worker binding (`QueryDatabaseBinding`) or a scoped API token.
20 *
21 * Provide it on an {@link Action} (or any deploy-time Effect) so you can talk to
22 * a D1 database with the same `prepare`/`exec`/`batch` client you'd use inside a
23 * Worker:
24 *
25 * @example Seeding a database from an Action
26 * ```typescript
27 * const Seed = Alchemy.Action(
28 * "Seed",
29 * Effect.gen(function* () {
30 * const db = yield* Cloudflare.D1.QueryDatabase(database);
31 * return Effect.fn(function* () {
32 * yield* db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)");
33 * yield* db
34 * .prepare("INSERT INTO users (id, name) VALUES (?, ?)")
35 * .bind("1", "Ada")
36 * .run();
37 * });
38 * }).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseLocal)),
39 * );
40 * ```
41 *
42 * The database id is resolved at apply time through the ambient
43 * {@link RuntimeContext} (in an Action, that's the resolve context the engine
44 * provides around the body), so `QueryDatabase(database)` works even though the
45 * database is created in the same deploy.
46 */
47export const QueryDatabaseLocal = Layer.effect(
48 QueryDatabase,
49 Effect.gen(function* () {
50 // Capture the account resolver without authenticating. Only the HTTP
51 // branch evaluates it; native local clients need no cloud credentials.
52 // Credentials are ambient during stack-eval (the stack's
53 // providers layer). Capture the full context so the HTTP query ops run
54 // with the current credentials — no `host.bind`, no minted token.
55 const environment = yield* CloudflareEnvironment;
56 const context = yield* Effect.context<
57 Credentials | HttpClient.HttpClient
58 >();
59 // The FULL ambient context, for the dev-mode gateway: booting an
60 // ephemeral workerd needs the platform services (FileSystem, Path,
61 // spawner, HttpClient) and the Cloudflare environment, all of which are
62 // present during stack-eval but not statically enumerable here.
63 const ambient = yield* Effect.context<never>();
64
65 // Under `alchemy dev` the database may be a local simulator row (a
66 // `dev:` id). Queries then tunnel through an ephemeral workerd gateway
67 // into the simulator's DO SQLite (one gateway boot per query — slow but
68 // correct; Actions are deploy-time one-shots). Real ids keep the cloud
69 // HTTP query API.
70 const localTransport =
71 (databaseId: string) =>
72 (
73 body:
74 | { sql: string; params?: unknown[] }
75 | { batch: { sql: string; params?: unknown[] }[] },
76 ) =>
77 withLocalD1Query(databaseId, (query) => query(body)).pipe(
78 Effect.provide(localD1GatewayRuntime),
79 // The gateway layer's platform requirements are satisfied by the
80 // ambient stack-eval context captured above; `Context<never>`
81 // can't prove that statically, so erase the leftover R.
82 Effect.provideContext(ambient),
83 (eff) => Effect.runPromise(eff as Effect.Effect<never, never>),
84 ) as Promise<{
85 result: Array<{
86 results?: unknown;
87 success?: boolean | null;
88 meta?: unknown;
89 }>;
90 }>;
91
92 return Effect.fn(function* (database: Database) {
93 // Deferred accessor — resolves the databaseId against the tracker at
94 // apply time. No `host.bind`: the local variant registers no binding.
95 const databaseId = yield* database.databaseId;
96 return makeQueryDatabaseClientFrom(
97 Effect.flatMap(databaseId, (id) =>
98 isLocalId(id)
99 ? Effect.succeed(makeD1DatabaseFromTransport(localTransport(id)))
100 : Effect.map(environment, ({ accountId }) =>
101 makeHttpD1Database(
102 {
103 accountId,
104 authorize: (eff) =>
105 eff.pipe(Effect.provideContext(context)),
106 },
107 id,
108 ),
109 ),
110 ),
111 );
112 });
113 }),
114);