Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .oxlintrc.effect.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"!apps/cli/src/command-internal/db-*.ts",
"!apps/cli/src/command-internal/dotenv.*",
"!apps/cli/src/command-internal/experimental-feature.ts",
"!apps/cli/src/command-internal/experimental-gate.*",
"!apps/cli/src/command-internal/local-project-context.*",
"!apps/cli/src/command-internal/pgpass.*",
"!apps/cli/src/command-internal/pgservicefile.*",
Expand All @@ -41,6 +42,7 @@
"!apps/cli/src/command-internal/stack-shadow.ts",
"!apps/cli/src/command-internal/stack-storage.ts",
"!apps/cli/src/command-internal/vault-decrypt.*",
"!apps/cli/src/command-internal/vault.*"
"!apps/cli/src/command-internal/vault.*",
"!apps/cli/src/command-internal/viper-env.*"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ export const buildLocalDbContainerInputs = (
// See {@link resolveDockerNetworkMode} for the full flag/env/fallback precedence.
const networkId = resolveDockerNetworkMode({
explicit: Option.getOrUndefined(networkIdFlag),
envOverride: viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues),
envOverride: yield* viperEnvStringWithProjectFallback(
"SUPABASE_NETWORK_ID",
projectEnvValues,
),
projectId,
});
// Only needed on Linux; Docker Desktop already resolves `host.docker.internal` elsewhere.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
FAKE_EMPTY_TAR,
FAKE_UNSTAMPED_PGDATA_TAR,
fakePgDataTar,
withConfigEnv,
withEmptyConfigEnv,
withEnvVar,
mockDockerDaemonCliSpawner,
useTempWorkdir,
Expand Down Expand Up @@ -52,7 +54,9 @@ const defaultConfig: CliConfig = decodeConfig({});
const tempRoot = useTempWorkdir("shadow-cache-");

const withShadowCacheEnv = <A, E, R>(value: string | undefined, body: Effect.Effect<A, E, R>) =>
withEnvVar(SHADOW_CACHE_ENV, value, body);
value === undefined
? withEmptyConfigEnv(body)
: withConfigEnv({ [SHADOW_CACHE_ENV]: value }, body);

/**
* Isolates the global shadow-baseline cache under a per-test `SUPABASE_HOME` so tests never
Expand Down Expand Up @@ -183,8 +187,11 @@ describe("acquireShadowDatabase", () => {
const path = yield* Path.Path;
const peek = yield* peekShadowBaseline(shadowInput(fs, path));
expect(peek).toEqual({ state: "uncachable" });
}).pipe(Effect.provideService(ConfigProvider.ConfigProvider, configProvider)),
).pipe(Effect.provide(Layer.mergeAll(BunServices.layer, out.layer)));
}),
).pipe(
Effect.provideService(ConfigProvider.ConfigProvider, configProvider),
Effect.provide(Layer.mergeAll(BunServices.layer, out.layer)),
);
});

it.live("is today's bare create when the cache is explicitly disabled", () => {
Expand Down
15 changes: 11 additions & 4 deletions apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,10 @@ export const peekShadowBaseline = <E>(
Effect.gen(function* () {
if (
opts.bypassCache === true ||
!viperEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {})
!(yield* viperEnvBoolWithProjectFallback(
SHADOW_CACHE_ENV,
input.setup.projectEnvValues ?? {},
))
) {
return { state: "uncachable" } as const;
}
Expand Down Expand Up @@ -868,9 +871,13 @@ export const acquireShadowDatabase = <E>(
Effect.gen(function* () {
if (
opts.bypassCache === true ||
!viperEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}, {
whenUnset: true,
})
!(yield* viperEnvBoolWithProjectFallback(
SHADOW_CACHE_ENV,
input.setup.projectEnvValues ?? {},
{
whenUnset: true,
},
))
) {
yield* annotateCacheState("disabled");
return yield* uncachedShadow(spawner, input);
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/command-internal/db-pull-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
fileOpen: true,
});
const stackBackend = (yield* currentStackBackend).kind === "stack";
const seedNetwork = dumpNetworkMode(
const seedNetwork = yield* dumpNetworkMode(
Option.getOrUndefined(networkIdFlag),
stackBackend,
projectEnv,
Expand Down
125 changes: 82 additions & 43 deletions apps/cli/src/command-internal/docker-ids.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { describe, expect, it } from "@effect/vitest";
import { ConfigProvider, Effect } from "effect";

import {
CLI_PROJECT_LABEL,
Expand Down Expand Up @@ -77,53 +78,91 @@ describe("cliProjectFilterValue", () => {
describe("resolveDockerNetworkMode composed with viperEnvStringWithProjectFallback (start/db start call shape)", () => {
const KEY = "SUPABASE_NETWORK_ID";

afterEach(() => {
delete process.env[KEY];
});
const withShellEnv = (env: Readonly<Record<string, string>>) =>
Effect.provide(
ConfigProvider.layer(ConfigProvider.fromEnvRecord(env, { preserveEmptyStrings: true })),
);

function resolve(flagValue: string | undefined, projectEnv: Record<string, string>) {
return resolveDockerNetworkMode({
explicit: flagValue,
envOverride: viperEnvStringWithProjectFallback(KEY, projectEnv),
projectId: "my-app",
return Effect.gen(function* () {
return resolveDockerNetworkMode({
explicit: flagValue,
envOverride: yield* viperEnvStringWithProjectFallback(KEY, projectEnv),
projectId: "my-app",
});
});
}

it("prefers an explicit --network-id flag over everything else", () => {
process.env[KEY] = "env-network";
expect(resolve("flag-network", { [KEY]: "toml-network" })).toBe("flag-network");
});

it("falls back to SUPABASE_NETWORK_ID (shell) when the flag is absent", () => {
process.env[KEY] = "shell-network";
expect(resolve(undefined, {})).toBe("shell-network");
});

it("falls back to SUPABASE_NETWORK_ID (project .env) when both the flag and shell are absent", () => {
delete process.env[KEY];
expect(resolve(undefined, { [KEY]: "project-network" })).toBe("project-network");
});

it("prefers the shell value over the project .env value (presence wins, matching godotenv.Load)", () => {
process.env[KEY] = "shell-network";
expect(resolve(undefined, { [KEY]: "project-network" })).toBe("shell-network");
});

it("falls back to the generated network name when the flag and env are all absent/empty", () => {
delete process.env[KEY];
expect(resolve(undefined, {})).toBe(localNetworkId("my-app"));
expect(resolve("", {})).toBe(localNetworkId("my-app"));
});

it("an explicit-but-empty --network-id= skips the env var entirely (viper: a Changed pflag resolves before AutomaticEnv)", () => {
process.env[KEY] = "env-network";
expect(resolve("", { [KEY]: "project-network" })).toBe(localNetworkId("my-app"));
});

it("treats an empty shell value as present (blocks the project value) and falls to generated", () => {
process.env[KEY] = "";
expect(resolve(undefined, { [KEY]: "project-network" })).toBe(localNetworkId("my-app"));
});
it.effect("prefers an explicit --network-id flag over everything else", () =>
Effect.gen(function* () {
expect(
yield* resolve("flag-network", { [KEY]: "toml-network" }).pipe(
withShellEnv({ [KEY]: "env-network" }),
),
).toBe("flag-network");
}),
);

it.effect("falls back to SUPABASE_NETWORK_ID (shell) when the flag is absent", () =>
Effect.gen(function* () {
expect(yield* resolve(undefined, {}).pipe(withShellEnv({ [KEY]: "shell-network" }))).toBe(
"shell-network",
);
}),
);

it.effect(
"falls back to SUPABASE_NETWORK_ID (project .env) when both the flag and shell are absent",
() =>
Effect.gen(function* () {
expect(yield* resolve(undefined, { [KEY]: "project-network" }).pipe(withShellEnv({}))).toBe(
"project-network",
);
}),
);

it.effect(
"prefers the shell value over the project .env value (presence wins, matching godotenv.Load)",
() =>
Effect.gen(function* () {
expect(
yield* resolve(undefined, { [KEY]: "project-network" }).pipe(
withShellEnv({ [KEY]: "shell-network" }),
),
).toBe("shell-network");
}),
);

it.effect(
"falls back to the generated network name when the flag and env are all absent/empty",
() =>
Effect.gen(function* () {
expect(yield* resolve(undefined, {}).pipe(withShellEnv({}))).toBe(localNetworkId("my-app"));
expect(yield* resolve("", {}).pipe(withShellEnv({}))).toBe(localNetworkId("my-app"));
}),
);

it.effect(
"an explicit-but-empty --network-id= skips the env var entirely (viper: a Changed pflag resolves before AutomaticEnv)",
() =>
Effect.gen(function* () {
expect(
yield* resolve("", { [KEY]: "project-network" }).pipe(
withShellEnv({ [KEY]: "env-network" }),
),
).toBe(localNetworkId("my-app"));
}),
);

it.effect(
"treats an empty shell value as present (blocks the project value) and falls to generated",
() =>
Effect.gen(function* () {
expect(
yield* resolve(undefined, { [KEY]: "project-network" }).pipe(withShellEnv({ [KEY]: "" })),
).toBe(localNetworkId("my-app"));
}),
);
});

describe("sanitizeProjectId", () => {
Expand Down
49 changes: 24 additions & 25 deletions apps/cli/src/command-internal/experimental-gate.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Layer } from "effect";
import { ConfigProvider, Effect, Exit, Layer } from "effect";

import { CliArgs } from "../shared/cli/cli-args.service.ts";
import { ExperimentalFlag } from "./global-flags.ts";
Expand All @@ -8,6 +8,8 @@ import { ExperimentalRequiredError, requireExperimental } from "./experimental-g
const ENV = "SUPABASE_EXPERIMENTAL";
const withFlag = (value: boolean, args: ReadonlyArray<string> = []) =>
Layer.mergeAll(Layer.succeed(ExperimentalFlag, value), Layer.succeed(CliArgs, { args }));
const withShellEnv = (env: Readonly<Record<string, string>>) =>
ConfigProvider.layer(ConfigProvider.fromEnvRecord(env, { preserveEmptyStrings: true }));

describe("requireExperimental", () => {
it.effect("passes when --experimental is set", () =>
Expand All @@ -16,39 +18,35 @@ describe("requireExperimental", () => {

it.effect("fails with Go's byte-exact message when neither flag nor env is set", () =>
Effect.gen(function* () {
const saved = process.env[ENV];
delete process.env[ENV];
const error = yield* requireExperimental.pipe(Effect.provide(withFlag(false)), Effect.flip);
if (saved === undefined) delete process.env[ENV];
else process.env[ENV] = saved;
const error = yield* requireExperimental.pipe(
Effect.provide(Layer.merge(withFlag(false), withShellEnv({}))),
Effect.flip,
);
expect(error).toBeInstanceOf(ExperimentalRequiredError);
expect(error.message).toBe("must set the --experimental flag to run this command");
}),
);

it.effect("passes when SUPABASE_EXPERIMENTAL=1 even without the flag (viper AutomaticEnv)", () =>
Effect.gen(function* () {
const saved = process.env[ENV];
process.env[ENV] = "1";
const exit = yield* requireExperimental.pipe(Effect.provide(withFlag(false)), Effect.exit);
if (saved === undefined) delete process.env[ENV];
else process.env[ENV] = saved;
expect(exit._tag).toBe("Success");
const exit = yield* requireExperimental.pipe(
Effect.provide(Layer.merge(withFlag(false), withShellEnv({ [ENV]: "1" }))),
Effect.exit,
);
expect(Exit.isSuccess(exit)).toBe(true);
}),
);

it.effect(
"fails even with SUPABASE_EXPERIMENTAL=1 when --experimental=false is explicit (viper Changed wins)",
() =>
Effect.gen(function* () {
const saved = process.env[ENV];
process.env[ENV] = "1";
const error = yield* requireExperimental.pipe(
Effect.provide(withFlag(false, ["--experimental=false"])),
Effect.provide(
Layer.merge(withFlag(false, ["--experimental=false"]), withShellEnv({ [ENV]: "1" })),
),
Effect.flip,
);
if (saved === undefined) delete process.env[ENV];
else process.env[ENV] = saved;
expect(error).toBeInstanceOf(ExperimentalRequiredError);
}),
);
Expand All @@ -57,15 +55,16 @@ describe("requireExperimental", () => {
"passes with SUPABASE_EXPERIMENTAL=1 when --experimental=false is a positional operand after --",
() =>
Effect.gen(function* () {
const saved = process.env[ENV];
process.env[ENV] = "1";
const exit = yield* requireExperimental.pipe(
Effect.provide(withFlag(false, ["--", "--experimental=false"])),
Effect.provide(
Layer.merge(
withFlag(false, ["--", "--experimental=false"]),
withShellEnv({ [ENV]: "1" }),
),
),
Effect.exit,
);
if (saved === undefined) delete process.env[ENV];
else process.env[ENV] = saved;
expect(exit._tag).toBe("Success");
expect(Exit.isSuccess(exit)).toBe(true);
}),
);

Expand All @@ -79,7 +78,7 @@ describe("requireExperimental", () => {
),
Effect.exit,
);
expect(exit._tag).toBe("Success");
expect(Exit.isSuccess(exit)).toBe(true);
}),
);

Expand All @@ -103,7 +102,7 @@ describe("requireExperimental", () => {
Effect.provide(withFlag(false, ["db", "pull", "--experimental=false", "--experimental"])),
Effect.exit,
);
expect(exit._tag).toBe("Success");
expect(Exit.isSuccess(exit)).toBe(true);
}),
);
});
10 changes: 5 additions & 5 deletions apps/cli/src/command-internal/global-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export const resolveYes = Effect.gen(function* () {
if (yesFlagExplicitlyFalse(cliArgs.args)) {
return false;
}
return flag || viperEnvBool("SUPABASE_YES");
return flag || (yield* viperEnvBool("SUPABASE_YES"));
});

/**
Expand All @@ -242,7 +242,7 @@ export const resolveYesWithProjectEnv = (projectEnv: Record<string, string>) =>
if (yesFlagExplicitlyFalse(cliArgs.args)) {
return false;
}
return flag || viperEnvBoolWithProjectFallback("SUPABASE_YES", projectEnv);
return flag || (yield* viperEnvBoolWithProjectFallback("SUPABASE_YES", projectEnv));
});

/**
Expand Down Expand Up @@ -276,7 +276,7 @@ export const resolveExperimental = Effect.gen(function* () {
if (explicit !== undefined) {
return explicit;
}
return flag || viperEnvBool("SUPABASE_EXPERIMENTAL");
return flag || (yield* viperEnvBool("SUPABASE_EXPERIMENTAL"));
});

/**
Expand All @@ -291,7 +291,7 @@ export const resolveExperimentalWithProjectEnv = (projectEnv: Record<string, str
if (explicit !== undefined) {
return explicit;
}
return flag || viperEnvBoolWithProjectFallback("SUPABASE_EXPERIMENTAL", projectEnv);
return flag || (yield* viperEnvBoolWithProjectFallback("SUPABASE_EXPERIMENTAL", projectEnv));
});

/**
Expand Down Expand Up @@ -327,5 +327,5 @@ export const resolveDebugWithProjectEnv = (projectEnv: Record<string, string>) =
if (debugFlagExplicitlyFalse(cliArgs.args)) {
return false;
}
return flag || viperEnvBoolWithProjectFallback("SUPABASE_DEBUG", projectEnv);
return flag || (yield* viperEnvBoolWithProjectFallback("SUPABASE_DEBUG", projectEnv));
});
Loading
Loading