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