From 0e6b98b9bb3e4feeded0953f70242bc1f148deda Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 17 Aug 2026 16:32:04 +0200 Subject: [PATCH 1/4] feat(gen): add dart and json output languages to gen types --- apps/cli/docs/go-cli-divergences.md | 7 ++ .../legacy/commands/gen/types/SIDE_EFFECTS.md | 9 +- .../commands/gen/types/types.command.ts | 6 +- .../commands/gen/types/types.handler.ts | 64 +++++++++- .../gen/types/types.integration.test.ts | 119 ++++++++++++++++++ .../legacy/commands/gen/types/types.shared.ts | 23 ++++ 6 files changed, 222 insertions(+), 6 deletions(-) diff --git a/apps/cli/docs/go-cli-divergences.md b/apps/cli/docs/go-cli-divergences.md index ce946eb4d5..c19cc9a753 100644 --- a/apps/cli/docs/go-cli-divergences.md +++ b/apps/cli/docs/go-cli-divergences.md @@ -28,6 +28,13 @@ These commands exist in the TS CLI today but have no direct top-level equivalent Under the `SUPABASE_USE_PG_DELTA_NEXT=false` legacy opt-out the flag is accepted but has no effect, since the legacy edge-runtime engine does not emit coverage diagnostics. Default behavior (omitted flag) matches Go. +- `gen types` has two TS-only `--lang` values (the Go reference accepted only + `typescript`/`go`/`swift`/`python`): `json` emits pg-meta's language-neutral + generator metadata document (for third-party type generators), and `dart` + pipes that document through the `supabase_typegen` Dart package on the host, + requiring the Dart SDK on PATH and `supabase_typegen` as a dev dependency of + the current project. Both require a pg-meta image that ships the `json` + generator (supabase/postgres-meta#1106). - `db push` has a TS-only `--skip-vault` flag. It applies migrations without resolving or updating `[db.vault]` secrets; default behavior still matches Go. - Every legacy command that resolves a linked project ref for its own database diff --git a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md index 6441e6b534..9d19299be1 100644 --- a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md @@ -51,11 +51,17 @@ config for that ref to build the fallback connection (the saved workdir | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------- | | `docker`/`podman container inspect supabase_db_` | `--local` | assert `supabase start` is running | | `docker`/`podman run --rm --network --env … node dist/server/server.js` | `--local`, `--db-url`, project-ref paths with non-TypeScript `--lang` | run pg-meta to generate types from a live database | +| `dart run supabase_typegen --input - --output -` | `--lang dart`, after the pg-meta container exits successfully | turn pg-meta's json metadata into Dart source | A raw TCP `SSLRequest` probe is also opened to the target database host/port to detect TLS support before launching pg-meta, with the default 10s pg-delta probe timeout. +For `--lang dart` the pg-meta container runs the `json` generator; its stdout is +collected instead of printed and piped to the Dart typegen's stdin. The Dart SDK +must be on PATH and `supabase_typegen` must be a dev dependency of the current +project. + ## Environment Variables | Variable | Purpose | Required? | @@ -108,7 +114,8 @@ Not applicable. - With `--local`, a missing `supabase/config.toml` uses the embedded config defaults plus shell and nested dotenv overrides, matching the legacy CLI. - **Sanctioned intentional divergence (CLI-1988 parity ruling):** - `--lang` accepts `typescript` (default), `go`, `swift`, or `python`. Project-ref paths + `--lang` accepts `typescript` (default), `go`, `swift`, `python`, `dart`, or `json` + (the last two are TS-only, see `docs/go-cli-divergences.md`). Project-ref paths (`--linked`, `--project-id`, and the implicit linked fallback) use the Management API for TypeScript, and run pg-meta locally against the project database (temporary login-role credentials, preview-branch fallback) for the other languages. The old Go diff --git a/apps/cli/src/legacy/commands/gen/types/types.command.ts b/apps/cli/src/legacy/commands/gen/types/types.command.ts index 113a3a5413..f57e73508a 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.command.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.command.ts @@ -6,7 +6,7 @@ import { legacyParseSchemaFlags } from "../../../shared/legacy-schema-flags.ts"; import { legacyGenTypes } from "./types.handler.ts"; import { legacyGenTypesRuntimeLayer } from "./types.layers.ts"; -const LANG_VALUES = ["typescript", "go", "swift", "python"] as const; +const LANG_VALUES = ["typescript", "go", "swift", "python", "dart", "json"] as const; const SWIFT_ACCESS_CONTROL_VALUES = ["internal", "public"] as const; const config = { @@ -77,6 +77,10 @@ export const legacyGenTypesCommand = Command.make("types", commandConfig).pipe( command: "supabase gen types --db-url 'postgresql://...' --schema public --schema auth", description: "Generate types from a database URL", }, + { + command: "supabase gen types --local --lang=dart", + description: "Generate Dart types from the local dev database", + }, ]), Command.withHandler((flags) => legacyGenTypes(flags).pipe( diff --git a/apps/cli/src/legacy/commands/gen/types/types.handler.ts b/apps/cli/src/legacy/commands/gen/types/types.handler.ts index 1bab0dda3d..76b75023ca 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.handler.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.handler.ts @@ -1,5 +1,5 @@ import { loadProjectConfig } from "@supabase/config"; -import { ChildProcessSpawner } from "effect/unstable/process"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { Effect, FileSystem, Option, Path, Stdio, Stream } from "effect"; import { LegacyDnsResolverFlag, @@ -46,6 +46,9 @@ import { LegacyGenTypesNetworkError, LegacyGenTypesUnexpectedStatusError } from import { legacyGetHostname } from "../../../shared/legacy-hostname.ts"; import { LegacyPlatformApiFactory } from "../../../auth/legacy-platform-api-factory.service.ts"; import { + DART_TYPEGEN_ARGS, + DART_TYPEGEN_COMMAND, + DART_TYPEGEN_UNAVAILABLE_MESSAGE, defaultSchemas, buildPostgresUrl, localDbContainerId, @@ -257,6 +260,10 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le const schemas = flags.schema; const lang = flags.lang; const swiftAccessControl = flags.swiftAccessControl; + // Dart types are produced by piping pg-meta's language-neutral `json` + // generator metadata through the supabase_typegen package, so the container + // always runs the json generator for `--lang dart`. + const pgMetaLang = lang === "dart" ? "json" : lang; const loadConfig = () => loadProjectConfig(cliConfig.workdir, { goViperCompat: true }); const loadConfigForRef = (projectRef: string) => @@ -416,7 +423,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le `PG_META_DB_URL=${target.url}`, `PG_CONN_TIMEOUT_SECS=${queryTimeoutSeconds}`, `PG_QUERY_TIMEOUT_SECS=${queryTimeoutSeconds}`, - `PG_META_GENERATE_TYPES=${lang}`, + `PG_META_GENERATE_TYPES=${pgMetaLang}`, `PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=${input.includedSchemas}`, `PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL=${swiftAccessControl}`, `PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=${String(!input.postgrestV9Compat)}`, @@ -456,11 +463,21 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le stderr: "pipe", }); + // For `--lang dart` the container emits the json generator + // metadata, which is piped into the Dart typegen after the run + // instead of being printed. + let stdoutText = ""; let stderrText = ""; const [exitCode] = yield* Effect.all( [ child.exitCode.pipe(Effect.map(Number)), - forwardByteStream(child.stdout, (text) => output.raw(text, "stdout")), + forwardByteStream(child.stdout, (text) => + lang === "dart" + ? Effect.sync(() => { + stdoutText += text; + }) + : output.raw(text, "stdout"), + ), forwardByteStream(child.stderr, (text) => Effect.sync(() => { stderrText += text; @@ -469,7 +486,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le ], { concurrency: "unbounded" }, ); - return { exitCode, stderrText }; + return { exitCode, stderrText, stdoutText }; }); const runTarget = (conn: LegacyPgConnInput) => @@ -498,6 +515,45 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le if (result.exitCode !== 0) { return yield* Effect.fail(new Error(`error running container: exit ${result.exitCode}`)); } + + if (lang === "dart") { + yield* runDartTypegen(result.stdoutText); + } + }), + ); + + // Pipes the json generator metadata into the supabase_typegen package on + // the host: the generated Dart code arrives on stdout like every other + // language, and the package's own summary line stays on stderr. + const runDartTypegen = (metadataJson: string) => + Effect.scoped( + Effect.gen(function* () { + const child = yield* spawner + .spawn( + ChildProcess.make(DART_TYPEGEN_COMMAND, DART_TYPEGEN_ARGS, { + stdin: Stream.succeed(new TextEncoder().encode(metadataJson)), + stdout: "pipe", + stderr: "pipe", + }), + ) + .pipe(Effect.mapError(() => new Error(DART_TYPEGEN_UNAVAILABLE_MESSAGE))); + + const [exitCode] = yield* Effect.all( + [ + child.exitCode.pipe(Effect.map(Number)), + forwardByteStream(child.stdout, (text) => output.raw(text, "stdout")), + forwardByteStream(child.stderr, (text) => output.raw(text, "stderr")), + ], + { concurrency: "unbounded" }, + ); + if (exitCode !== 0) { + return yield* Effect.fail( + new Error( + `error running the supabase_typegen package: exit ${exitCode}. ` + + "Add supabase_typegen as a dev dependency of the current project to generate Dart types.", + ), + ); + } }), ); diff --git a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts index ba4950784c..8fdab4abd1 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts @@ -63,6 +63,8 @@ import { legacyGenCommand } from "../gen.command.ts"; import type { LegacyGenTypesFlags } from "./types.command.ts"; import { legacyGenTypes } from "./types.handler.ts"; import { + DART_TYPEGEN_ARGS, + DART_TYPEGEN_COMMAND, localDbContainerId, localNetworkId, parseQueryTimeoutSeconds, @@ -1213,6 +1215,123 @@ describe("legacy gen types", () => { }), ); + it.live("passes json straight through to pg-meta for --lang json", () => + Effect.tryPromise({ + try: () => + withSslProbeServer(async (port) => { + const docker = captureDockerRun(); + const { layer, out } = setup({ + args: [ + "gen", + "types", + "--db-url", + `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`, + "--lang", + "json", + ], + childStdout: ['{"version":1}'], + onSpawn: docker.onSpawn, + }); + + await Effect.runPromise( + legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), + lang: "json", + }), + ).pipe(Effect.provide(layer)), + ); + + expect(docker.env.has("PG_META_GENERATE_TYPES=json")).toBe(true); + expect(out.stdoutText).toContain('{"version":1}'); + }), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }), + ); + + it.live("pipes pg-meta's json metadata through the Dart typegen for --lang dart", () => + Effect.tryPromise({ + try: () => + withSslProbeServer(async (port) => { + // First spawn is the pg-meta container emitting the json metadata, + // second is the host `dart run supabase_typegen` invocation emitting + // the generated code. + const child = mockSequentialChildProcessSpawner([ + { exitCode: 0, stdout: ['{"version":1,"tables":[]}'] }, + { exitCode: 0, stdout: ["// generated dart"] }, + ]); + const { layer, out } = setup({ + args: [ + "gen", + "types", + "--db-url", + `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`, + "--lang", + "dart", + ], + childLayer: child.layer, + }); + + await Effect.runPromise( + legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), + lang: "dart", + }), + ).pipe(Effect.provide(layer)), + ); + + expect(child.spawned).toHaveLength(2); + const dockerRun = child.spawned[0]; + expect(dockerRun?.args).toContain("PG_META_GENERATE_TYPES=json"); + const dart = child.spawned[1]; + expect(dart?.command).toBe(DART_TYPEGEN_COMMAND); + expect(dart?.args).toEqual([...DART_TYPEGEN_ARGS]); + // The metadata is piped to the Dart typegen, never printed; only the + // generated code reaches stdout. + expect(out.stdoutText).toContain("// generated dart"); + expect(out.stdoutText).not.toContain('"version"'); + }), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }), + ); + + it.live("fails with an actionable error when the Dart typegen exits non-zero", () => + Effect.tryPromise({ + try: () => + withSslProbeServer(async (port) => { + const child = mockSequentialChildProcessSpawner([ + { exitCode: 0, stdout: ['{"version":1,"tables":[]}'] }, + { exitCode: 65, stderr: ["Could not find package `supabase_typegen`"] }, + ]); + const { layer } = setup({ + args: [ + "gen", + "types", + "--db-url", + `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`, + "--lang", + "dart", + ], + childLayer: child.layer, + }); + + const exit = await Effect.runPromiseExit( + legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), + lang: "dart", + }), + ).pipe(Effect.provide(layer)), + ); + + expect(Exit.isFailure(exit)).toBe(true); + expect(String(exit)).toContain("error running the supabase_typegen package: exit 65"); + }), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }), + ); + it.live("allows --postgrest-v9-compat together with --db-url", () => Effect.tryPromise({ try: () => diff --git a/apps/cli/src/legacy/commands/gen/types/types.shared.ts b/apps/cli/src/legacy/commands/gen/types/types.shared.ts index 4480a03ada..d1c4853c80 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.shared.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.shared.ts @@ -42,6 +42,29 @@ export function defaultSchemas(extraSchemas: ReadonlyArray = []) { return [...new Set(["public", ...extraSchemas])]; } +/** + * `--lang dart` pipes pg-meta's `json` generator metadata through the + * `supabase_typegen` Dart package. The host's Dart SDK runs the package from + * the current working directory, so it must be declared as a (dev) dependency + * of the project the command runs in — the invocation and its stdin/stdout + * contract (`--input -` reads the metadata document from stdin, `--output -` + * writes the generated code to stdout) are pinned here so the handler and + * tests share one definition. + */ +export const DART_TYPEGEN_COMMAND = "dart"; + +export const DART_TYPEGEN_ARGS = [ + "run", + "supabase_typegen", + "--input", + "-", + "--output", + "-", +] as const; + +export const DART_TYPEGEN_UNAVAILABLE_MESSAGE = + "Generating Dart types requires the Dart SDK on PATH. Install it from https://dart.dev/get-dart and add the supabase_typegen package as a dev dependency of the current project."; + export function parseQueryTimeoutSeconds( raw: string, ): Effect.Effect { From 6dc68f8146d5e873c8eae7d6b279a343fafd0c12 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 09:40:57 +0200 Subject: [PATCH 2/4] fix(gen): forward the schema selection to the dart typegen and reject multi-schema runs --- apps/cli/docs/go-cli-divergences.md | 5 +- .../legacy/commands/gen/types/SIDE_EFFECTS.md | 6 +- .../commands/gen/types/types.handler.ts | 40 +++++---- .../gen/types/types.integration.test.ts | 81 ++++++++++++++++++- .../legacy/commands/gen/types/types.shared.ts | 5 +- 5 files changed, 115 insertions(+), 22 deletions(-) diff --git a/apps/cli/docs/go-cli-divergences.md b/apps/cli/docs/go-cli-divergences.md index c19cc9a753..33b2cdfafe 100644 --- a/apps/cli/docs/go-cli-divergences.md +++ b/apps/cli/docs/go-cli-divergences.md @@ -33,8 +33,9 @@ These commands exist in the TS CLI today but have no direct top-level equivalent generator metadata document (for third-party type generators), and `dart` pipes that document through the `supabase_typegen` Dart package on the host, requiring the Dart SDK on PATH and `supabase_typegen` as a dev dependency of - the current project. Both require a pg-meta image that ships the `json` - generator (supabase/postgres-meta#1106). + the current project. `dart` generates one schema per run and therefore + accepts at most one `--schema`. Both require a pg-meta image that ships the + `json` generator (supabase/postgres-meta#1106). - `db push` has a TS-only `--skip-vault` flag. It applies migrations without resolving or updating `[db.vault]` secrets; default behavior still matches Go. - Every legacy command that resolves a linked project ref for its own database diff --git a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md index 9d19299be1..978ea8e4e6 100644 --- a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md @@ -51,7 +51,7 @@ config for that ref to build the fallback connection (the saved workdir | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------- | | `docker`/`podman container inspect supabase_db_` | `--local` | assert `supabase start` is running | | `docker`/`podman run --rm --network --env … node dist/server/server.js` | `--local`, `--db-url`, project-ref paths with non-TypeScript `--lang` | run pg-meta to generate types from a live database | -| `dart run supabase_typegen --input - --output -` | `--lang dart`, after the pg-meta container exits successfully | turn pg-meta's json metadata into Dart source | +| `dart run supabase_typegen --input - --output - --schema ` | `--lang dart`, after the pg-meta container exits successfully | turn pg-meta's json metadata into Dart source | A raw TCP `SSLRequest` probe is also opened to the target database host/port to detect TLS support before launching pg-meta, with the default 10s pg-delta probe @@ -60,7 +60,9 @@ timeout. For `--lang dart` the pg-meta container runs the `json` generator; its stdout is collected instead of printed and piped to the Dart typegen's stdin. The Dart SDK must be on PATH and `supabase_typegen` must be a dev dependency of the current -project. +project. The typegen generates one schema per run, so `--lang dart` accepts at +most one `--schema` (defaulting to `public`) and fails otherwise instead of +silently dropping schemas. ## Environment Variables diff --git a/apps/cli/src/legacy/commands/gen/types/types.handler.ts b/apps/cli/src/legacy/commands/gen/types/types.handler.ts index 76b75023ca..639174b4f2 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.handler.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.handler.ts @@ -464,20 +464,17 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le }); // For `--lang dart` the container emits the json generator - // metadata, which is piped into the Dart typegen after the run - // instead of being printed. - let stdoutText = ""; + // metadata, which is collected and piped into the Dart typegen + // after the run instead of being printed. let stderrText = ""; - const [exitCode] = yield* Effect.all( + const [exitCode, stdoutText] = yield* Effect.all( [ child.exitCode.pipe(Effect.map(Number)), - forwardByteStream(child.stdout, (text) => - lang === "dart" - ? Effect.sync(() => { - stdoutText += text; - }) - : output.raw(text, "stdout"), - ), + lang === "dart" + ? collectByteStream(child.stdout) + : forwardByteStream(child.stdout, (text) => output.raw(text, "stdout")).pipe( + Effect.map(() => ""), + ), forwardByteStream(child.stderr, (text) => Effect.sync(() => { stderrText += text; @@ -498,6 +495,18 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le probePort: conn.port, }); + // The Dart typegen generates one schema per run; without this guard a + // multi-schema selection would silently come out as only one schema. + const dartSchemas = input.includedSchemas.split(",").filter((schema) => schema.length > 0); + if (lang === "dart" && dartSchemas.length > 1) { + return yield* Effect.fail( + new Error( + `--lang dart generates one schema per run, but got: ${input.includedSchemas}. ` + + "Pass a single --schema and run the command once per schema.", + ), + ); + } + const result = input.poolerFallback === undefined ? yield* buildRun(input) @@ -517,7 +526,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le } if (lang === "dart") { - yield* runDartTypegen(result.stdoutText); + yield* runDartTypegen(result.stdoutText, dartSchemas[0] ?? "public"); } }), ); @@ -525,12 +534,12 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le // Pipes the json generator metadata into the supabase_typegen package on // the host: the generated Dart code arrives on stdout like every other // language, and the package's own summary line stays on stderr. - const runDartTypegen = (metadataJson: string) => + const runDartTypegen = (metadataJson: string, schema: string) => Effect.scoped( Effect.gen(function* () { const child = yield* spawner .spawn( - ChildProcess.make(DART_TYPEGEN_COMMAND, DART_TYPEGEN_ARGS, { + ChildProcess.make(DART_TYPEGEN_COMMAND, [...DART_TYPEGEN_ARGS, "--schema", schema], { stdin: Stream.succeed(new TextEncoder().encode(metadataJson)), stdout: "pipe", stderr: "pipe", @@ -550,7 +559,8 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le return yield* Effect.fail( new Error( `error running the supabase_typegen package: exit ${exitCode}. ` + - "Add supabase_typegen as a dev dependency of the current project to generate Dart types.", + "If the package could not be resolved, add supabase_typegen as a dev " + + "dependency of the current project to generate Dart types.", ), ); } diff --git a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts index 8fdab4abd1..4ad6a7deb4 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts @@ -1286,7 +1286,7 @@ describe("legacy gen types", () => { expect(dockerRun?.args).toContain("PG_META_GENERATE_TYPES=json"); const dart = child.spawned[1]; expect(dart?.command).toBe(DART_TYPEGEN_COMMAND); - expect(dart?.args).toEqual([...DART_TYPEGEN_ARGS]); + expect(dart?.args).toEqual([...DART_TYPEGEN_ARGS, "--schema", "public"]); // The metadata is piped to the Dart typegen, never printed; only the // generated code reaches stdout. expect(out.stdoutText).toContain("// generated dart"); @@ -1332,6 +1332,85 @@ describe("legacy gen types", () => { }), ); + it.live("forwards a single non-default schema to the Dart typegen", () => + Effect.tryPromise({ + try: () => + withSslProbeServer(async (port) => { + const child = mockSequentialChildProcessSpawner([ + { exitCode: 0, stdout: ['{"version":1,"tables":[]}'] }, + { exitCode: 0, stdout: ["// generated dart"] }, + ]); + const { layer } = setup({ + args: [ + "gen", + "types", + "--db-url", + `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`, + "--lang", + "dart", + "--schema", + "sales", + ], + childLayer: child.layer, + }); + + await Effect.runPromise( + legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), + lang: "dart", + schema: ["sales"], + }), + ).pipe(Effect.provide(layer)), + ); + + const dart = child.spawned[1]; + expect(dart?.args).toEqual([...DART_TYPEGEN_ARGS, "--schema", "sales"]); + }), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }), + ); + + it.live("rejects --lang dart with more than one schema", () => + Effect.tryPromise({ + try: () => + withSslProbeServer(async (port) => { + const child = mockSequentialChildProcessSpawner([]); + const { layer } = setup({ + args: [ + "gen", + "types", + "--db-url", + `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`, + "--lang", + "dart", + "--schema", + "public", + "--schema", + "sales", + ], + childLayer: child.layer, + }); + + const exit = await Effect.runPromiseExit( + legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), + lang: "dart", + schema: ["public", "sales"], + }), + ).pipe(Effect.provide(layer)), + ); + + expect(Exit.isFailure(exit)).toBe(true); + expect(String(exit)).toContain("--lang dart generates one schema per run"); + // The guard fires before any container is spawned. + expect(child.spawned).toHaveLength(0); + }), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }), + ); + it.live("allows --postgrest-v9-compat together with --db-url", () => Effect.tryPromise({ try: () => diff --git a/apps/cli/src/legacy/commands/gen/types/types.shared.ts b/apps/cli/src/legacy/commands/gen/types/types.shared.ts index d1c4853c80..a123b2c5bd 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.shared.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.shared.ts @@ -46,10 +46,11 @@ export function defaultSchemas(extraSchemas: ReadonlyArray = []) { * `--lang dart` pipes pg-meta's `json` generator metadata through the * `supabase_typegen` Dart package. The host's Dart SDK runs the package from * the current working directory, so it must be declared as a (dev) dependency - * of the project the command runs in — the invocation and its stdin/stdout + * of the project the command runs in. The invocation and its stdin/stdout * contract (`--input -` reads the metadata document from stdin, `--output -` * writes the generated code to stdout) are pinned here so the handler and - * tests share one definition. + * tests share one definition; the handler appends `--schema ` because + * the package generates one schema per run. */ export const DART_TYPEGEN_COMMAND = "dart"; From eeebe95fec4776b530d730afa01cd95d17ec9752 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 13:49:44 +0200 Subject: [PATCH 3/4] docs(gen): reference the postgrest-typegen GeneratorMetadata contract --- apps/cli/docs/go-cli-divergences.md | 16 +++++++++------- .../commands/gen/types/types.integration.test.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/cli/docs/go-cli-divergences.md b/apps/cli/docs/go-cli-divergences.md index 33b2cdfafe..0b49f6ea77 100644 --- a/apps/cli/docs/go-cli-divergences.md +++ b/apps/cli/docs/go-cli-divergences.md @@ -29,13 +29,15 @@ These commands exist in the TS CLI today but have no direct top-level equivalent accepted but has no effect, since the legacy edge-runtime engine does not emit coverage diagnostics. Default behavior (omitted flag) matches Go. - `gen types` has two TS-only `--lang` values (the Go reference accepted only - `typescript`/`go`/`swift`/`python`): `json` emits pg-meta's language-neutral - generator metadata document (for third-party type generators), and `dart` - pipes that document through the `supabase_typegen` Dart package on the host, - requiring the Dart SDK on PATH and `supabase_typegen` as a dev dependency of - the current project. `dart` generates one schema per run and therefore - accepts at most one `--schema`. Both require a pg-meta image that ships the - `json` generator (supabase/postgres-meta#1106). + `typescript`/`go`/`swift`/`python`): `json` emits the language-neutral + `GeneratorMetadata` introspection document (the `@supabase/postgrest-typegen` + contract, for third-party type generators), and `dart` pipes that document + through the `supabase_typegen` Dart package on the host, requiring the Dart + SDK on PATH and `supabase_typegen` as a dev dependency of the current + project. `dart` generates one schema per run and therefore accepts at most + one `--schema`. Both require a pg-meta image that ships a `json` output for + `PG_META_GENERATE_TYPES` (pending in postgres-meta after + supabase/postgres-meta#1084 lands the postgrest-typegen extraction). - `db push` has a TS-only `--skip-vault` flag. It applies migrations without resolving or updating `[db.vault]` secrets; default behavior still matches Go. - Every legacy command that resolves a linked project ref for its own database diff --git a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts index 4ad6a7deb4..3413f25e02 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts @@ -1229,7 +1229,7 @@ describe("legacy gen types", () => { "--lang", "json", ], - childStdout: ['{"version":1}'], + childStdout: ['{"tables":[],"columns":[]}'], onSpawn: docker.onSpawn, }); @@ -1243,7 +1243,7 @@ describe("legacy gen types", () => { ); expect(docker.env.has("PG_META_GENERATE_TYPES=json")).toBe(true); - expect(out.stdoutText).toContain('{"version":1}'); + expect(out.stdoutText).toContain('{"tables":[],"columns":[]}'); }), catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), }), @@ -1257,7 +1257,7 @@ describe("legacy gen types", () => { // second is the host `dart run supabase_typegen` invocation emitting // the generated code. const child = mockSequentialChildProcessSpawner([ - { exitCode: 0, stdout: ['{"version":1,"tables":[]}'] }, + { exitCode: 0, stdout: ['{"tables":[],"columns":[]}'] }, { exitCode: 0, stdout: ["// generated dart"] }, ]); const { layer, out } = setup({ @@ -1290,7 +1290,7 @@ describe("legacy gen types", () => { // The metadata is piped to the Dart typegen, never printed; only the // generated code reaches stdout. expect(out.stdoutText).toContain("// generated dart"); - expect(out.stdoutText).not.toContain('"version"'); + expect(out.stdoutText).not.toContain('"columns"'); }), catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), }), @@ -1301,7 +1301,7 @@ describe("legacy gen types", () => { try: () => withSslProbeServer(async (port) => { const child = mockSequentialChildProcessSpawner([ - { exitCode: 0, stdout: ['{"version":1,"tables":[]}'] }, + { exitCode: 0, stdout: ['{"tables":[],"columns":[]}'] }, { exitCode: 65, stderr: ["Could not find package `supabase_typegen`"] }, ]); const { layer } = setup({ @@ -1337,7 +1337,7 @@ describe("legacy gen types", () => { try: () => withSslProbeServer(async (port) => { const child = mockSequentialChildProcessSpawner([ - { exitCode: 0, stdout: ['{"version":1,"tables":[]}'] }, + { exitCode: 0, stdout: ['{"tables":[],"columns":[]}'] }, { exitCode: 0, stdout: ["// generated dart"] }, ]); const { layer } = setup({ From d2cbcc210f5ebf106c871cb3682a0f15b8ed7c25 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 14:08:54 +0200 Subject: [PATCH 4/4] docs(gen): point the json output dependency at postgres-meta#1110 --- apps/cli/docs/go-cli-divergences.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli/docs/go-cli-divergences.md b/apps/cli/docs/go-cli-divergences.md index 0b49f6ea77..1c3c6321b2 100644 --- a/apps/cli/docs/go-cli-divergences.md +++ b/apps/cli/docs/go-cli-divergences.md @@ -36,8 +36,8 @@ These commands exist in the TS CLI today but have no direct top-level equivalent SDK on PATH and `supabase_typegen` as a dev dependency of the current project. `dart` generates one schema per run and therefore accepts at most one `--schema`. Both require a pg-meta image that ships a `json` output for - `PG_META_GENERATE_TYPES` (pending in postgres-meta after - supabase/postgres-meta#1084 lands the postgrest-typegen extraction). + `PG_META_GENERATE_TYPES` (supabase/postgres-meta#1110, stacked on the #1084 + postgrest-typegen extraction). - `db push` has a TS-only `--skip-vault` flag. It applies migrations without resolving or updating `[db.vault]` secrets; default behavior still matches Go. - Every legacy command that resolves a linked project ref for its own database