diff --git a/packages/alchemy/src/Platform.ts b/packages/alchemy/src/Platform.ts index e03955521c..796c449f2c 100644 --- a/packages/alchemy/src/Platform.ts +++ b/packages/alchemy/src/Platform.ts @@ -43,6 +43,12 @@ export interface PlatformProps { isExternal?: boolean; } +/** Synthetic Config nodes such as an absent `Config.option` have nothing to bind. */ +const isBindableRuntimeConfigNode = ( + path: ReadonlyArray, + value: unknown, +): boolean => path.length > 0 && value !== undefined; + /** * Provide the platform class's layer (`cls.make(props, impl)`) with a * lifetime that matches the phase. @@ -569,14 +575,22 @@ export const Platform = < path.map((p) => p.toString()).join("_"), ); const node = yield* configProvider.load(path); - if (phase === "plan" && node) { + if ( + phase === "plan" && + node && + isBindableRuntimeConfigNode(path, node.value) + ) { // bind it to the RuntimeContext if running in plan phase const output = Output.literal( Redacted.make(node.value), ); yield* ctx?.set(key, output) ?? Effect.void; return node; - } else if (phase === "runtime" && ctx) { + } else if ( + phase === "runtime" && + ctx && + key.length > 0 + ) { // retrieve from the RuntimeContext if running in runtime phase const value = yield* ctx.get>(key); diff --git a/packages/alchemy/test/PlatformConfigBindings.test.ts b/packages/alchemy/test/PlatformConfigBindings.test.ts new file mode 100644 index 0000000000..64d269fb3b --- /dev/null +++ b/packages/alchemy/test/PlatformConfigBindings.test.ts @@ -0,0 +1,83 @@ +import { Platform } from "@/Platform.ts"; +import * as Provider from "@/Provider.ts"; +import type { Resource } from "@/Resource.ts"; +import { inMemoryState } from "@/State/index.ts"; +import * as Test from "@/Test/Alchemy.ts"; +import { expect } from "alchemy-test"; +import * as Config from "effect/Config"; +import * as ConfigProvider from "effect/ConfigProvider"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +interface ConfigHost extends Resource< + "Test.ConfigHost", + { env?: Record }, + { ready: true }, + { env?: Record } +> {} + +const boundKeys: string[] = []; + +const ConfigHost: any = Platform("Test.ConfigHost", { + createRuntimeContext: (id) => ({ + Type: "Test.ConfigHost", + id, + env: {}, + set: (key) => + Effect.sync(() => { + boundKeys.push(key); + return key; + }), + get: () => Effect.succeed(undefined), + }), +}); + +const providers = Provider.succeed(ConfigHost, { + list: () => Effect.succeed([]), + diff: Effect.fn(function* () { + return undefined; + }), + reconcile: Effect.fn(function* () { + return { ready: true as const }; + }), + delete: Effect.fn(function* () {}), +}); + +const { test } = Test.make({ providers, state: inMemoryState() }); + +test.provider( + "an absent optional field skips synthetic binding and preserves concrete config bindings", + (stack) => + Effect.gen(function* () { + boundKeys.length = 0; + + yield* stack.deploy( + ConfigHost( + "ConfigHost", + {}, + Effect.gen(function* () { + const optional = yield* Config.schema( + Schema.Struct({ maybe: Schema.optional(Schema.String) }), + ); + expect(optional).toEqual({}); + + const concrete = yield* Config.string( + "ALCHEMY_TEST_PRESENT_CONFIG", + ); + expect(concrete).toBe("present"); + return {}; + }), + ), + ); + + expect(boundKeys).toEqual(["ALCHEMY_TEST_PRESENT_CONFIG"]); + }).pipe( + Effect.provide( + ConfigProvider.layer( + ConfigProvider.fromUnknown({ + ALCHEMY_TEST_PRESENT_CONFIG: "present", + }), + ), + ), + ), +);