| 1 | import * as PgClient from "@effect/sql-pg/PgClient"; |
| 2 | import * as Context from "effect/Context"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import type * as Redacted from "effect/Redacted"; |
| 6 | import * as Sql from "effect/sql/SqlClient"; |
| 7 | import { makeExecutionMemo } from "../Runtime/ExecutionMemo.ts"; |
| 8 | import { proxyChain } from "../Util/proxy-chain.ts"; |
| 9 | import { 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 | */ |
| 16 | export 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 | */ |
| 52 | export 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 | */ |
| 86 | export 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)))); |