Skip to content

Commit e41db48

Browse files
committed
fix(cli): expand env(VAR) in test db config.toml port/password fields
Go decodes config.toml through mapstructure with LoadEnvHook (pkg/config/decode_hooks.go), so any string field of the form env(VAR) resolves to the named environment variable when it is set and non-empty. The test db toml reader passed [db].port/shadow_port/password through literally, so an env-backed local stack config connected to 54322/postgres instead of the configured values. Expand env(VAR) for these fields, preserving the literal when the variable is unset to match LoadEnvHook.
1 parent c07d70d commit e41db48

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,38 @@ function asRecord(value: unknown): RawDoc | undefined {
4242
: undefined;
4343
}
4444

45+
const ENV_PATTERN = /^env\((.*)\)$/;
46+
47+
/**
48+
* Expand Go's `env(VAR)` config form. Mirrors `LoadEnvHook`
49+
* (`apps/cli-go/pkg/config/decode_hooks.go`): a string matching `^env\((.*)\)$`
50+
* resolves to the named environment variable, but only when that variable is set
51+
* and non-empty; otherwise the literal value is preserved unchanged (Go's hook
52+
* keeps `value` when `len(os.Getenv(name)) == 0`).
53+
*/
54+
function expandEnv(value: string): string {
55+
const matches = ENV_PATTERN.exec(value);
56+
if (matches !== null) {
57+
const env = process.env[matches[1] ?? ""];
58+
if (env !== undefined && env.length > 0) return env;
59+
}
60+
return value;
61+
}
62+
63+
/**
64+
* Resolve a `[db]` port field. Go decodes the TOML string/number into a `uint`
65+
* with `mapstructure`'s weakly-typed input *after* `LoadEnvHook` runs, so an
66+
* `env(VAR)` reference (written as a quoted string) is expanded and then parsed
67+
* as the port. A plain number is used directly; anything that does not resolve
68+
* to a non-negative integer falls back to the default.
69+
*/
4570
function numberOr(value: unknown, fallback: number): number {
46-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
71+
if (typeof value === "number" && Number.isFinite(value)) return value;
72+
if (typeof value === "string") {
73+
const expanded = expandEnv(value);
74+
if (/^\d+$/.test(expanded)) return Number(expanded);
75+
}
76+
return fallback;
4777
}
4878

4979
function nonEmptyString(value: unknown): Option.Option<string> {
@@ -111,7 +141,7 @@ export const legacyReadDbToml = Effect.fnUntraced(function* (
111141
const values: LegacyDbTomlValues = {
112142
port: numberOr(db?.["port"], DEFAULT_PORT),
113143
shadowPort: numberOr(db?.["shadow_port"], DEFAULT_SHADOW_PORT),
114-
password: typeof db?.["password"] === "string" ? db["password"] : DEFAULT_PASSWORD,
144+
password: typeof db?.["password"] === "string" ? expandEnv(db["password"]) : DEFAULT_PASSWORD,
115145
poolerConnectionString,
116146
projectId,
117147
};

apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,44 @@ describe("legacyReadDbToml", () => {
123123
);
124124
});
125125

126+
it.effect("expands env(VAR) for password and port like Go's LoadEnvHook", () => {
127+
process.env["LEGACY_DB_PW"] = "from-env";
128+
process.env["LEGACY_DB_PORT"] = "6000";
129+
const dir = withConfig(
130+
["[db]", 'port = "env(LEGACY_DB_PORT)"', 'password = "env(LEGACY_DB_PW)"', ""].join("\n"),
131+
);
132+
return read(dir).pipe(
133+
Effect.tap((v) =>
134+
Effect.sync(() => {
135+
expect(v.port).toBe(6000);
136+
expect(v.password).toBe("from-env");
137+
delete process.env["LEGACY_DB_PW"];
138+
delete process.env["LEGACY_DB_PORT"];
139+
rmSync(dir, { recursive: true, force: true });
140+
}),
141+
),
142+
);
143+
});
144+
145+
it.effect("keeps the literal value and default port when the env var is unset/empty", () => {
146+
// Go's LoadEnvHook only substitutes when len(os.Getenv(name)) > 0; otherwise it
147+
// preserves the literal string, so a quoted env() port that cannot resolve falls
148+
// back to the schema default rather than parsing the literal.
149+
delete process.env["LEGACY_DB_UNSET"];
150+
const dir = withConfig(
151+
["[db]", 'port = "env(LEGACY_DB_UNSET)"', 'password = "env(LEGACY_DB_UNSET)"', ""].join("\n"),
152+
);
153+
return read(dir).pipe(
154+
Effect.tap((v) =>
155+
Effect.sync(() => {
156+
expect(v.port).toBe(54322);
157+
expect(v.password).toBe("env(LEGACY_DB_UNSET)");
158+
rmSync(dir, { recursive: true, force: true });
159+
}),
160+
),
161+
);
162+
});
163+
126164
it.effect("ignores a [db.pooler] connection_string in config.toml (Go reads .temp only)", () => {
127165
// The Go config field is tagged `toml:"-"`, so a connection_string in config.toml
128166
// is never honored; only supabase/.temp/pooler-url counts.

0 commit comments

Comments
 (0)