1import * as Effect from "effect/Effect";
2import * as Redacted from "effect/Redacted";
3import * as Output from "../Output.ts";
4import type { RuntimeContext } from "../RuntimeContext.ts";
5import { sha256 } from "../Util/sha256.ts";
6
7/**
8 * A database connection-string source, in every shape alchemy hands them
9 * out:
10 *
11 * - a literal / `Redacted` string
12 * - a resource Output (Neon `project.connectionUri`, PlanetScale
13 * `role.connectionUrl`, Prisma `connection.databaseUrl`, ...) — bound
14 * into the host environment at deploy and read back at runtime
15 * - a runtime-only Effect (Cloudflare Hyperdrive's per-invocation
16 * `connectionString`)
17 */
18export type ConnectionSource =
19 | string
20 | Redacted.Redacted<string>
21 | Output.Output<string>
22 | Output.Output<Redacted.Redacted<string>>
23 | Effect.Effect<Redacted.Redacted<string>, never, RuntimeContext>;
24
25/**
26 * The deploy-resolvable subset of {@link ConnectionSource}: literals and
27 * resource Outputs. Runtime-only Effects (Hyperdrive) are excluded —
28 * deploy-time consumers (migrations, seeds) need a value that resolves
29 * during stack evaluation.
30 */
31export type StaticConnectionSource = Exclude<
32 ConnectionSource,
33 Effect.Effect<Redacted.Redacted<string>, never, RuntimeContext>
34>;
35
36const toRedacted = (
37 value: string | Redacted.Redacted<string>,
38): Redacted.Redacted<string> =>
39 typeof value === "string" ? Redacted.make(value) : value;
40
41/**
42 * Resolve a {@link ConnectionSource} to its runtime accessor effect.
43 *
44 * Outputs are yielded NOW (binding into the host environment during a
45 * host init, or recording an Action capture during an Action init) and
46 * the returned accessor reads the resolved value back later; Effects pass
47 * through untouched; literals wrap.
48 */
49export const resolveConnectionSource = (
50 source: ConnectionSource,
51): Effect.Effect<Effect.Effect<Redacted.Redacted<string>>> =>
52 Effect.gen(function* () {
53 if (Output.isOutput(source)) {
54 const accessor = yield* source;
55 return Effect.map(
56 accessor as Effect.Effect<string | Redacted.Redacted<string>>,
57 toRedacted,
58 );
59 }
60 if (Effect.isEffect(source)) {
61 return source as Effect.Effect<Redacted.Redacted<string>>;
62 }
63 return Effect.succeed(toRedacted(source));
64 }) as Effect.Effect<Effect.Effect<Redacted.Redacted<string>>>;
65
66/**
67 * Pick the deploy-resolvable source for deploy-time work (migrations,
68 * seeds): an explicit override wins, `false` disables, and the primary
69 * source is a valid default only when it is itself deploy-resolvable.
70 */
71export const staticConnectionSource = (
72 source: ConnectionSource,
73 override: StaticConnectionSource | false | undefined,
74): StaticConnectionSource | undefined => {
75 if (override === false) {
76 return undefined;
77 }
78 if (override !== undefined) {
79 return override;
80 }
81 return Effect.isEffect(source) && !Output.isOutput(source)
82 ? undefined
83 : (source as StaticConnectionSource);
84};
85
86/**
87 * Non-secret digest of a connection source — a sha256 Output suitable for
88 * persisted identity inputs (e.g. a migration Action's diff key) where the
89 * connection string itself must never be stored.
90 */
91export const connectionSourceDigest = (
92 source: StaticConnectionSource,
93): Output.Output<string> => {
94 const digest = (value: string | Redacted.Redacted<string>) =>
95 sha256(typeof value === "string" ? value : Redacted.value(value));
96 return Output.isOutput(source)
97 ? (Output.mapEffect(digest)(
98 source as Output.Output<string | Redacted.Redacted<string>>,
99 ) as Output.Output<string>)
100 : (Output.fromEffect(digest(source)) as Output.Output<string>);
101};