Skip to content
Merged
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
10 changes: 9 additions & 1 deletion apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
| Path | Format | When |
| ----------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- |
| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` or `--project-id` |
| `<workdir>/supabase/config.toml` | TOML | when selecting schemas from config; required for `--local`, best-effort otherwise |
| `<workdir>/supabase/config.toml` | TOML | when selecting schemas; `--local` uses embedded defaults when the file is missing |
| `<workdir>{/supabase}/.env*` | dotenv | `--local`; resolves the same nested environment overrides as the legacy CLI |
| `<workdir>/supabase/.temp/rest-version` | plain text | `--local` only, when `db.major_version > 14` — forces v9 compat if the tag contains `v9` |
| `<workdir>/supabase/.temp/pgmeta-version` | plain text | `--local` only — overrides the pg-meta docker image tag |

Expand Down Expand Up @@ -59,6 +60,11 @@ default 10s pg-delta probe timeout.
| Variable | Purpose | Required? |
| ---------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token for linked/project-id mode | no (falls back to keyring → `~/.supabase/access-token`) |
| `SUPABASE_PROJECT_ID` | local Docker container and network project ID | no (falls back to the workdir name) |
| `SUPABASE_DB_PORT` | local database probe port | no (defaults to `54322`) |
| `SUPABASE_DB_MAJOR_VERSION` | local PostgreSQL major version | no (defaults to `17`) |
| `SUPABASE_API_SCHEMAS` | local schemas used when `--schema` is omitted | no (defaults to `public,graphql_public`) |
| `SUPABASE_ENV` | selects nested dotenv files for local generation | no (defaults to `development`) |
| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) |
| `SUPABASE_DB_PASSWORD` | database password for `--local` and the `--linked` workdir project | no (defaults to `postgres`; **ignored** for ad-hoc `--project-id`, which always mints a temporary login role) |
| `SUPABASE_SERVICES_HOSTNAME` | host used for the local TLS probe | no (defaults to `127.0.0.1`) |
Expand Down Expand Up @@ -95,6 +101,8 @@ Not applicable.
## Notes

- Exactly one of `--local`, `--linked`, `--project-id`, or `--db-url` must be specified.
- With `--local`, a missing `supabase/config.toml` uses the embedded config defaults plus
shell and nested dotenv overrides, matching the legacy CLI.
- `--lang` flag accepts `typescript` (default), `go`, `swift`, or `python`. Project-ref
paths use the Management API for TypeScript, and use a project database host +
temporary login role + pg-meta for other languages.
Expand Down
25 changes: 14 additions & 11 deletions apps/cli/src/legacy/commands/gen/types/types.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts";
import type { LegacyDbConfigFlags } from "../../../shared/legacy-db-config.types.ts";
import { legacyPoolerConfigFromConnectionString } from "../../../shared/legacy-db-config.parse.ts";
import {
legacyApplyProjectEnv,
legacyReadDbToml,
} from "../../../shared/legacy-db-config.toml-read.ts";
import type { LegacyPgConnInput } from "../../../shared/legacy-db-connection.service.ts";
import { legacyToPostgresURL } from "../../../shared/legacy-postgres-url.ts";
import { legacyTempPaths } from "../../../shared/legacy-temp-paths.ts";
Expand Down Expand Up @@ -527,20 +531,20 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le

yield* Effect.gen(function* () {
if (flags.local) {
const loaded = yield* loadConfig();
if (loaded === null) {
return yield* Effect.fail(
new Error("failed to load config: supabase/config.toml not found"),
);
}
const config = yield* legacyReadDbToml(fs, path, cliConfig.workdir);
Comment thread
7ttp marked this conversation as resolved.
yield* legacyApplyProjectEnv(
config.projectEnv,
Object.keys(config.projectEnv).filter((key) => key !== "SUPABASE_DB_PASSWORD"),
);
Comment thread
7ttp marked this conversation as resolved.
const projectId = Option.getOrElse(config.projectId, () => path.basename(cliConfig.workdir));

const paths = legacyTempPaths(path, cliConfig.workdir);
// Go resolves Config.Api.Image from the rest-version file only when
// Db.MajorVersion > 14, then forces v9 compat when that image tag contains "v9"
// (pkg/config/config.go:657-666, internal/gen/types/types.go:69). Gate and trim
// identically so we don't force v9 on older databases.
const restVersion =
loaded.config.db.major_version > 14
config.majorVersion > 14
? (yield* fs
.readFileString(paths.restVersion)
.pipe(Effect.orElseSucceed(() => ""))).trim()
Expand All @@ -551,9 +555,8 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
.pipe(Effect.orElseSucceed(() => ""));

const includedSchemas = (
schemas.length > 0 ? schemas : defaultSchemas(loaded.config.api.schemas)
schemas.length > 0 ? schemas : defaultSchemas(config.apiSchemas)
).join(",");
const projectId = loaded.config.project_id ?? path.basename(cliConfig.workdir);
yield* assertLocalDbRunning(projectId);

yield* runPgMeta({
Expand All @@ -567,7 +570,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
host: "db",
port: 5432,
probeHost: legacyGetHostname(),
probePort: loaded.config.db.port,
probePort: config.port,
networkMode: localNetworkId(projectId),
includedSchemas,
postgrestV9Compat: flags.postgrestV9Compat || forcedV9,
Expand Down Expand Up @@ -637,5 +640,5 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
schemas.length > 0 ? schemas : schemasFromConfig(loaded?.config.api.schemas),
false,
);
}).pipe(Effect.ensuring(telemetryState.flush));
}).pipe(Effect.scoped, Effect.ensuring(telemetryState.flush));
});
107 changes: 94 additions & 13 deletions apps/cli/src/legacy/commands/gen/types/types.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { basename, join } from "node:path";
import { describe, expect, it } from "@effect/vitest";
import { BunServices } from "@effect/platform-bun";
import type {
Expand Down Expand Up @@ -62,7 +62,12 @@ import type {
import { legacyGenCommand } from "../gen.command.ts";
import type { LegacyGenTypesFlags } from "./types.command.ts";
import { legacyGenTypes } from "./types.handler.ts";
import { parseQueryTimeoutSeconds, resolvePgmetaImage } from "./types.shared.ts";
import {
localDbContainerId,
localNetworkId,
parseQueryTimeoutSeconds,
resolvePgmetaImage,
} from "./types.shared.ts";

function writeConfig(workdir: string, contents: string) {
const supabaseDir = join(workdir, "supabase");
Expand Down Expand Up @@ -2631,22 +2636,98 @@ describe("legacy gen types", () => {
},
);

it.live("fails local generation when supabase/config.toml is missing", () => {
it.live("generates locally with Go defaults when supabase/config.toml is missing", () => {
const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-no-config-"));
const { layer } = setup({ workdir, skipConfig: true });
const docker = captureDockerRun();
const probes: Array<{ host: string; port: number }> = [];
const { layer, out, child } = setup({
workdir,
skipConfig: true,
childStdout: ["generated"],
onSpawn: docker.onSpawn,
sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, {
requireSsl: () => Effect.succeed(false),
requireSslForHost: (host, port) =>
Effect.sync(() => {
probes.push({ host, port });
return false;
}),
}),
});

return Effect.gen(function* () {
const exit = yield* legacyGenTypes(defaultFlags({ local: true })).pipe(
Effect.provide(layer),
Effect.exit,
yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer));

const projectId = basename(workdir);
expect(child.spawned[0]).toEqual({
command: "docker",
args: ["container", "inspect", localDbContainerId(projectId)],
});
expect(child.spawned[1]?.args).toContain(localNetworkId(projectId));
expect(probes).toEqual([{ host: "127.0.0.1", port: 54322 }]);
expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,graphql_public")).toBe(
true,
);
expect(out.stdoutText).toContain("generated");
});
});

expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(String(exit.cause)).toContain(
"failed to load config: supabase/config.toml not found",
);
}
it.live("honors local dotenv overrides when supabase/config.toml is missing", () => {
const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-no-config-env-"));
const supabaseDir = join(workdir, "supabase");
mkdirSync(supabaseDir, { recursive: true });
writeFileSync(
join(supabaseDir, ".env"),
[
"SUPABASE_PROJECT_ID=configless-env-project",
"SUPABASE_DB_PORT=55432",
"SUPABASE_DB_PASSWORD=remote-password",
"SUPABASE_API_SCHEMAS=private,graphql_public",
"SUPABASE_SERVICES_HOSTNAME=host.docker.internal",
"SUPABASE_INTERNAL_IMAGE_REGISTRY=mirror.example.com",
"",
].join("\n"),
);
const docker = captureDockerRun();
const probes: Array<{ host: string; port: number }> = [];
const { layer, out, child } = setup({
workdir,
skipConfig: true,
childStdout: ["generated"],
onSpawn: docker.onSpawn,
sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, {
requireSsl: () => Effect.succeed(false),
requireSslForHost: (host, port) =>
Effect.sync(() => {
probes.push({ host, port });
return false;
}),
}),
});

return Effect.gen(function* () {
yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer));

expect(child.spawned[0]).toEqual({
command: "docker",
args: ["container", "inspect", localDbContainerId("configless-env-project")],
});
expect(child.spawned[1]?.args).toContain(localNetworkId("configless-env-project"));
expect(probes).toEqual([{ host: "host.docker.internal", port: 55432 }]);
expect(
docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,private,graphql_public"),
).toBe(true);
expect(
docker.env.has(
"PG_META_DB_URL=postgresql://postgres:postgres@db:5432/postgres?connect_timeout=10",
),
).toBe(true);
expect(
child.spawned[1]?.args.some((arg) =>
arg.startsWith("mirror.example.com/supabase/postgres-meta:"),
),
).toBe(true);
expect(out.stdoutText).toContain("generated");
});
});

Expand Down
40 changes: 38 additions & 2 deletions apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type EnvLookup = (name: string) => string | undefined;
* and aborts the command rather than running against the default local database).
*/
interface LegacyDbTomlValues {
readonly projectEnv: Readonly<Record<string, string>>;
/**
* Resolves a `SUPABASE_*` env var with Go's precedence: shell env (non-empty)
* wins, then the loaded project `.env*` files (non-empty), else undefined.
Expand All @@ -51,6 +52,7 @@ interface LegacyDbTomlValues {
* rather than `process.env` alone (e.g. `SUPABASE_EXPERIMENTAL_PG_DELTA`).
*/
readonly envLookup: (name: string) => string | undefined;
readonly apiSchemas: ReadonlyArray<string>;
/** `[db] port`, default 54322 (`packages/config/src/db.ts`). */
readonly port: number;
/** `[db] shadow_port`, default 54320. */
Expand Down Expand Up @@ -169,6 +171,7 @@ const DEFAULT_PORT = 54322;
const DEFAULT_SHADOW_PORT = 54320;
const DEFAULT_MAJOR_VERSION = 17;
const DEFAULT_PASSWORD = "postgres";
const DEFAULT_API_SCHEMAS = ["public", "graphql_public"] as const;
/** `[edge_runtime] deno_version` default (`config.toml` template). 2 → the current edge-runtime image. */
const DEFAULT_DENO_VERSION = 2;

Expand Down Expand Up @@ -273,6 +276,7 @@ function legacyResolveValidatedRemoteProjectId(
* `AutomaticEnv` — `config.go:635-637`), so the block value must beat the env override.
*/
const LEGACY_ENV_OVERRIDABLE_KEYS: ReadonlyArray<string> = [
"api.schemas",
"db.port",
"db.shadow_port",
"db.major_version",
Expand Down Expand Up @@ -470,6 +474,22 @@ function resolveConfigInt(value: unknown, lookup: EnvLookup): number | "absent"
return "invalid";
}

function resolveStringSlice(
value: unknown,
fallback: ReadonlyArray<string>,
lookup: EnvLookup,
): ReadonlyArray<string> | undefined {
if (value === undefined) return fallback;
if (typeof value === "string") {
const expanded = legacyExpandEnv(value, lookup);
return expanded.length === 0 ? [] : expanded.split(",");
}
if (!Array.isArray(value) || !value.every((item): item is string => typeof item === "string")) {
return undefined;
}
return value.map((item) => legacyExpandEnv(item, lookup));
}

/**
* Replicates Go's `path.Join("supabase", pattern)` for a relative seed `sql_paths`
* entry (`pkg/config/config.go:881-886`). Go's `path.Join` runs `path.Clean`, which
Expand Down Expand Up @@ -603,9 +623,12 @@ export const legacyLoadProjectEnv = Effect.fnUntraced(function* (
* re-checks). The `acquireRelease` finalizer deletes only the keys it set when the
* scope closes, so in-process test workers don't leak env between cases.
*/
export const legacyApplyProjectEnv = (loaded: Record<string, string>) =>
export const legacyApplyProjectEnv = (
loaded: Readonly<Record<string, string>>,
keys: ReadonlyArray<string> = LEGACY_PROCESS_ENV_APPLY_KEYS,
) =>
Effect.forEach(
LEGACY_PROCESS_ENV_APPLY_KEYS,
keys,
(key) => {
const value = loaded[key];
if (value === undefined || process.env[key] !== undefined) {
Expand Down Expand Up @@ -1834,9 +1857,22 @@ const readDbTomlCore = Effect.fnUntraced(function* (
apiRaw?.["auto_expose_new_tables"],
lookup,
);
const apiSchemas = resolveStringSlice(
(remoteOverrideKeys.has("api.schemas") ? undefined : envOverride("SUPABASE_API_SCHEMAS")) ??
apiRaw?.["schemas"],
DEFAULT_API_SCHEMAS,
lookup,
);
if (apiSchemas === undefined) {
return yield* Effect.fail(
new LegacyDbConfigLoadError({ message: "failed to parse config: invalid api.schemas." }),
);
}

const values: LegacyDbTomlValues = {
projectEnv,
envLookup: envOverride,
apiSchemas,
port,
shadowPort,
password: passwordRaw !== undefined ? legacyExpandEnv(passwordRaw, lookup) : DEFAULT_PASSWORD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -929,11 +929,12 @@ describe("legacyReadDbToml", () => {
);
});

it.effect("keeps [api] auto_expose_new_tables tri-state None when absent", () => {
const dir = withConfig("[api]\n");
it.effect("decodes empty api schemas while keeping auto_expose_new_tables absent", () => {
const dir = withConfig('[api]\nschemas = ""\n');
return read(dir).pipe(
Effect.tap((v) =>
Effect.sync(() => {
expect(v.apiSchemas).toEqual([]);
expect(Option.isNone(v.baseline.apiAutoExposeNewTables)).toBe(true);
rmSync(dir, { recursive: true, force: true });
}),
Expand Down
Loading