1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Redacted from "effect/Redacted";
4import * as Binding from "../Binding.ts";
5import type { Resource } from "../Resource.ts";
6import {
7 ConnectPostgres,
8 PostgresUrlMissing,
9 type ConnectPostgresClient,
10} from "./ConnectPostgres.ts";
11import type { ServiceBinding } from "./MountVolume.ts";
12import { type Postgres } from "./Postgres.ts";
13
14const isFlyHost = (
15 value: unknown,
16): value is Resource<string, any, any, ServiceBinding> =>
17 typeof value === "object" &&
18 value !== null &&
19 ((value as { Type?: string }).Type === "Fly.Service" ||
20 (value as { Type?: string }).Type === "Fly.Machine");
21
22const asRedactedUrl = (
23 value: unknown,
24 name: string,
25): Effect.Effect<Redacted.Redacted<string>, PostgresUrlMissing> => {
26 const plain =
27 typeof value === "string"
28 ? value
29 : Redacted.isRedacted(value)
30 ? Redacted.value(value)
31 : "";
32 const url = typeof plain === "string" ? plain : "";
33 return url.length > 0
34 ? Effect.succeed(Redacted.make(url))
35 : Effect.fail(new PostgresUrlMissing({ name }));
36};
37
38/**
39 * Implementation of {@link ConnectPostgres}. Provide it on the
40 * {@link Service} Effect.
41 *
42 * Registers the cluster attachment for Fly's private network. Alchemy
43 * transports each cluster's connection URIs through resource Outputs,
44 * so callers do not configure connection-string environment variables.
45 *
46 *
47 * ### Provide the layer
48 * **Example:** On a Service
49 * ```typescript
50 * Effect.gen(function* () {
51 * const conn = yield* Fly.ConnectPostgres(Db);
52 * const db = yield* Drizzle.Postgres(conn.connectionString);
53 * }).pipe(Effect.provide(Fly.ConnectPostgresHttp))
54 * ```
55 *
56 * @layer
57 * @product Postgres
58 * @provides Fly.ConnectPostgres
59 */
60export const ConnectPostgresHttp = Layer.effect(
61 ConnectPostgres,
62 Effect.succeed(
63 Effect.fn(function* (postgres: Postgres) {
64 const name = postgres.LogicalId;
65 if (!globalThis.__ALCHEMY_RUNTIME__) {
66 const host = yield* Binding.Host;
67 if (isFlyHost(host)) {
68 yield* host.bind`${postgres}`({
69 postgres: { clusterId: postgres.clusterId },
70 });
71 }
72 }
73
74 const pooled = yield* postgres.pooledConnectionUri;
75 const direct = yield* postgres.connectionUri;
76
77 return {
78 connectionString: pooled.pipe(
79 Effect.flatMap((value) => asRedactedUrl(value, name)),
80 ),
81 directConnectionString: direct.pipe(
82 Effect.flatMap((value) => asRedactedUrl(value, name)),
83 ),
84 } satisfies ConnectPostgresClient;
85 }),
86 ),
87);