1import * as PgClient from "@effect/sql-pg/PgClient";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import type * as Redacted from "effect/Redacted";
6import * as Sql from "effect/sql/SqlClient";
7import { makeExecutionMemo } from "../Runtime/ExecutionMemo.ts";
8import { proxyChain } from "../Util/proxy-chain.ts";
9import { resolveConnectionOptions } from "./PostgresTls.ts";
10
11/**
12 * Options for {@link Postgres}: `@effect/sql-pg`'s pool configuration, with
13 * `url` widened to also accept an Effect (e.g. a Hyperdrive connection
14 * string, which resolves from the Worker environment at runtime).
15 */
16export type PostgresConfig<E, R> = Omit<PgClient.PgPoolConfig, "url"> & {
17 readonly url:
18 | Redacted.Redacted<string>
19 | Effect.Effect<Redacted.Redacted<string>, E, R>;
20};
21
22/**
23 * Open an `@effect/sql-pg` client (a connection pool) from a connection URL.
24 *
25 * Accepts a plain `Redacted` URL or an Effect of one — e.g.
26 * `Cloudflare.Hyperdrive.Connect(...)` or `Fly.ConnectPostgres(...)`'s
27 * `connectionString` — and returns a
28 * `PgClient` (which implements the generic `SqlClient` interface) wrapped in
29 * a chainable Proxy, so it can be resolved once at init and used from any
30 * handler:
31 *
32 * ```typescript
33 * import * as SQL from "alchemy/SQL/Postgres";
34 *
35 * const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
36 * const sql = yield* SQL.Postgres({ url: hd.connectionString });
37 *
38 * fetch: Effect.gen(function* () {
39 * const users = yield* sql`SELECT * FROM users`;
40 * });
41 * ```
42 *
43 * The pool is built lazily on the first query and memoized on the current
44 * execution's `Scope` (via {@link makeExecutionMemo}), so it's created at
45 * most once per execution — a Worker `fetch`/`queue`/`scheduled` event, a
46 * Durable Object call, a Workflow run, or a Lambda invocation — and its
47 * `end` finalizer fires when the event settles. Yielding the connection URL
48 * is likewise deferred, so deploy / plan-time invocations never connect.
49 *
50 * @binding
51 */
52export const Postgres = <E = never, R = never>(config: PostgresConfig<E, R>) =>
53 Effect.map(
54 makeExecutionMemo(
55 Effect.gen(function* () {
56 const { url, ...pool } = config;
57 const resolved = Effect.isEffect(url) ? yield* url : url;
58 const pgCtx = yield* Layer.build(
59 PgClient.layer({
60 ...pool,
61 ...resolveConnectionOptions(resolved, pool.ssl),
62 }),
63 );
64 return Context.get(pgCtx, PgClient.PgClient);
65 }),
66 ),
67 (client) => proxyChain<PgClient.PgClient>(client),
68 );
69
70/**
71 * Provide an `@effect/sql-pg` client as the `PgClient` and generic
72 * `SqlClient` services, so cloud-agnostic services written against
73 * `SqlClient.SqlClient` (or drizzle's `effect-postgres` driver, which
74 * depends on `PgClient`) run on the configured pool:
75 *
76 * ```typescript
77 * const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
78 * const app = yield* makeApp.pipe(
79 * Effect.provide(SQL.PostgresLayer({ url: hd.connectionString })),
80 * );
81 * ```
82 *
83 * The layer itself builds synchronously at init; the underlying pool is
84 * created lazily per execution (see {@link Postgres}).
85 */
86export const PostgresLayer = <E = never, R = never>(
87 config: PostgresConfig<E, R>,
88) =>
89 // Derive SqlClient from the single PgClient build so both tags share one
90 // per-execution pool.
91 Layer.effect(
92 Sql.SqlClient,
93 Effect.gen(function* () {
94 return yield* PgClient.PgClient;
95 }),
96 ).pipe(Layer.provideMerge(Layer.effect(PgClient.PgClient, Postgres(config))));