| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Redacted from "effect/Redacted"; |
| 3 | import * as Output from "../Output.ts"; |
| 4 | import type { RuntimeContext } from "../RuntimeContext.ts"; |
| 5 | import { 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 | */ |
| 18 | export 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 | */ |
| 31 | export type StaticConnectionSource = Exclude< |
| 32 | ConnectionSource, |
| 33 | Effect.Effect<Redacted.Redacted<string>, never, RuntimeContext> |
| 34 | >; |
| 35 | |
| 36 | const 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 | */ |
| 49 | export 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 | */ |
| 71 | export 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 | */ |
| 91 | export 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 | }; |