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 * as Output from "../Output.ts";
6import { unpackEnvValue, type RuntimeContext } from "../RuntimeContext.ts";
7import {
8 ConnectPostgres,
9 DATABASE_PUBLIC_URL_SECRET,
10 DATABASE_URL_SECRET,
11 PostgresUrlMissing,
12 connectEnvKeys,
13 type ConnectPostgresClient,
14} from "./ConnectPostgres.ts";
15import { isRailwayHost } from "./MountVolume.ts";
16import type { Postgres } from "./Postgres.ts";
17
18const runtimeOutput = <A>(
19 key: string,
20 output: Output.Output<A>,
21): Effect.Effect<A, never, RuntimeContext> =>
22 output.bind(key).pipe(Effect.flatMap((effect) => effect));
23
24const asRedactedUrl = (
25 value: string,
26 name: string,
27): Effect.Effect<Redacted.Redacted<string>, PostgresUrlMissing> =>
28 value.length > 0
29 ? Effect.succeed(Redacted.make(value))
30 : Effect.fail(new PostgresUrlMissing({ name }));
31
32const fromProcessEnv = (key: string): string => {
33 const unpacked = unpackEnvValue<unknown>(process.env[key]);
34 if (typeof unpacked === "string") return unpacked;
35 if (Redacted.isRedacted(unpacked)) {
36 const inner = Redacted.value(unpacked);
37 return typeof inner === "string" ? inner : "";
38 }
39 return "";
40};
41
42const firstUrl = (
43 values: ReadonlyArray<string>,
44 name: string,
45): Effect.Effect<Redacted.Redacted<string>, PostgresUrlMissing> => {
46 const found = values.find((value) => value.length > 0);
47 return found !== undefined
48 ? asRedactedUrl(found, name)
49 : Effect.fail(new PostgresUrlMissing({ name }));
50};
51
52/**
53 * Implementation of {@link ConnectPostgres}. Provide it on the
54 * {@link Service} or {@link Function} Effect.
55 *
56 * At deploy time this packs the private connection URI onto the host
57 * (`RAILWAY_POSTGRES_*`, `DATABASE_URL`). At runtime the client reads
58 * `process.env`.
59 *
60 *
61 * ### Provide the layer
62 * **Example:** On a Function or Service
63 * ```typescript
64 * Effect.gen(function* () {
65 * const conn = yield* Railway.ConnectPostgres(Db);
66 * const db = yield* Drizzle.Postgres(conn.connectionString);
67 * }).pipe(Effect.provide(Railway.ConnectPostgresHttp))
68 * ```
69 *
70 * @layer
71 * @product Postgres
72 * @provides Railway.ConnectPostgres
73 */
74export const ConnectPostgresHttp = Layer.effect(
75 ConnectPostgres,
76 Effect.succeed(
77 Effect.fn(function* (postgres: Postgres) {
78 const keys = connectEnvKeys(postgres);
79 const name = postgres.LogicalId;
80 if (!globalThis.__ALCHEMY_RUNTIME__) {
81 const host = yield* Binding.Host;
82 if (isRailwayHost(host)) {
83 yield* host.bind`${postgres}`({
84 env: {
85 [keys.pooled]: postgres.connectionUri,
86 [keys.direct]: postgres.connectionUri,
87 [DATABASE_URL_SECRET]: postgres.connectionUri,
88 },
89 });
90 }
91 }
92
93 const fromEnv = (preferDirect: boolean) =>
94 firstUrl(
95 preferDirect
96 ? [
97 fromProcessEnv(keys.direct),
98 fromProcessEnv(DATABASE_URL_SECRET),
99 fromProcessEnv(keys.pooled),
100 fromProcessEnv(DATABASE_PUBLIC_URL_SECRET),
101 ]
102 : [
103 fromProcessEnv(keys.pooled),
104 fromProcessEnv(DATABASE_URL_SECRET),
105 fromProcessEnv(keys.direct),
106 fromProcessEnv(DATABASE_PUBLIC_URL_SECRET),
107 ],
108 name,
109 );
110
111 if (globalThis.__ALCHEMY_RUNTIME__) {
112 return {
113 connectionString: fromEnv(false),
114 directConnectionString: fromEnv(true),
115 } satisfies ConnectPostgresClient;
116 }
117
118 const pooled = runtimeOutput(keys.pooled, postgres.connectionUri);
119 const direct = runtimeOutput(keys.direct, postgres.connectionUri);
120
121 return {
122 connectionString: Effect.gen(function* () {
123 const packed = yield* pooled;
124 const unpacked = yield* direct;
125 return yield* firstUrl(
126 [
127 typeof packed === "string" ? packed : "",
128 fromProcessEnv(keys.pooled),
129 fromProcessEnv(DATABASE_URL_SECRET),
130 typeof unpacked === "string" ? unpacked : "",
131 fromProcessEnv(keys.direct),
132 fromProcessEnv(DATABASE_PUBLIC_URL_SECRET),
133 ],
134 name,
135 );
136 }),
137 directConnectionString: Effect.gen(function* () {
138 const unpacked = yield* direct;
139 const packed = yield* pooled;
140 return yield* firstUrl(
141 [
142 typeof unpacked === "string" ? unpacked : "",
143 fromProcessEnv(keys.direct),
144 fromProcessEnv(DATABASE_URL_SECRET),
145 typeof packed === "string" ? packed : "",
146 fromProcessEnv(keys.pooled),
147 fromProcessEnv(DATABASE_PUBLIC_URL_SECRET),
148 ],
149 name,
150 );
151 }),
152 } satisfies ConnectPostgresClient;
153 }),
154 ),
155);