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
9 changes: 9 additions & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@
"@effect/vitest": "catalog:",
"@modelcontextprotocol/sdk": "^1.30.0",
"@napi-rs/keyring": "^1.3.0",
"@oxfmt/binding-darwin-arm64": "0.65.0",
"@oxfmt/binding-darwin-x64": "0.65.0",
"@oxfmt/binding-linux-arm64-gnu": "0.65.0",
"@oxfmt/binding-linux-arm64-musl": "0.65.0",
"@oxfmt/binding-linux-x64-gnu": "0.65.0",
"@oxfmt/binding-linux-x64-musl": "0.65.0",
"@oxfmt/binding-win32-arm64-msvc": "0.65.0",
"@oxfmt/binding-win32-x64-msvc": "0.65.0",
"@parcel/watcher": "^2.6.0",
"@parcel/watcher-darwin-arm64": "2.6.0",
"@parcel/watcher-darwin-x64": "2.6.0",
Expand All @@ -70,6 +78,7 @@
"@supabase/config": "workspace:*",
"@supabase/pg-delta": "1.0.0-alpha.46",
"@supabase/pg-topo": "1.0.0-alpha.5",
"@supabase/postgrest-typegen": "0.2.0",
"@supabase/process-compose": "workspace:*",
"@supabase/stack": "workspace:*",
"@tsconfig/bun": "catalog:",
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/scripts/build-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { $ } from "bun";
import process from "node:process";

import { bundleServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts";
import { oxfmtExternalArgs } from "./bundle-externals.ts";

/**
* Compile a single CLI shell to a standalone binary, embedding the pre-bundled
Expand Down Expand Up @@ -30,4 +31,4 @@ const defineArg = `--define=SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE=${JSON.string
await bundleServeMainTemplate(),
)}`;

await $`bun build ${entrypoint} --compile ${versionDefine} ${defineArg} --outfile ${outfile}`;
await $`bun build ${entrypoint} --compile ${versionDefine} ${defineArg} ${oxfmtExternalArgs} --outfile ${outfile}`;
3 changes: 3 additions & 0 deletions apps/cli/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from "node:path";
import process from "node:process";
import { parseArgs } from "node:util";
import { bundleServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts";
import { oxfmtExternalArgs } from "./bundle-externals.ts";
import { darwinBinariesForShell, MACOS_IDENTIFIERS } from "./macos-signing.ts";

const MUSL_TARGETS = [
Expand Down Expand Up @@ -152,6 +153,7 @@ async function buildTarget(target: (typeof TARGETS)[number]) {
`--define=SUPABASE_LIBC=${JSON.stringify(libc)}`,
serveMainTemplateDefine,
...posthogBuildDefines,
...oxfmtExternalArgs,
`--outfile=${outfile}`,
]);
console.log(`[${target.pkg}] Done.`);
Expand Down Expand Up @@ -301,6 +303,7 @@ async function buildMuslBinaries() {
`--define=SUPABASE_LIBC=${JSON.stringify(libc)}`,
serveMainTemplateDefine,
...posthogBuildDefines,
...oxfmtExternalArgs,
`--outfile=${outfile}`,
]);

Expand Down
22 changes: 22 additions & 0 deletions apps/cli/scripts/bundle-externals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Optional prettier plugins that `oxfmt`'s dist lazily `import()`s for
* non-TypeScript file types (liquid, pug, astro, …). They are not installed —
* gen types only ever formats generated TypeScript, through the statically
* embedded binding in `src/legacy/commands/gen/types/types.oxfmt.ts` — but
* `bun build` still tries to resolve every analyzable dynamic import, so each
* one must be marked external for the compile to succeed. Shared by the dev
* build (`build-binary.ts`) and the multi-target release build (`build.ts`).
*/
export const OXFMT_OPTIONAL_PLUGIN_EXTERNALS = [
"@prettier/plugin-hermes",
"@prettier/plugin-oxc",
"@prettier/plugin-pug",
"@shopify/prettier-plugin-liquid",
"@zackad/prettier-plugin-twig",
"prettier-plugin-astro",
"prettier-plugin-marko",
] as const;

export const oxfmtExternalArgs = OXFMT_OPTIONAL_PLUGIN_EXTERNALS.map(
(name) => `--external=${name}`,
);
145 changes: 93 additions & 52 deletions apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md

Large diffs are not rendered by default.

This file was deleted.

This file was deleted.

This file was deleted.

19 changes: 1 addition & 18 deletions apps/cli/src/legacy/commands/gen/types/types.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
ensureImage,
resolveDeadline,
} from "../../../../../tests/helpers/docker-image.ts";
import { resolvePgmetaImage } from "./types.shared.ts";

const TYPEGEN_LANGS = ["typescript", "go", "swift", "python"] as const;
type TypegenLang = (typeof TYPEGEN_LANGS)[number];
Expand Down Expand Up @@ -209,26 +208,12 @@ async function waitForLocalPostgres(containerName: string) {
);
}

// `gen types` starts pg-meta itself (local AND remote non-ts languages) via a
// single-registry rewrite with no fallback (`resolvePgmetaImage`), so pre-resolve
// it and retag the winning candidate onto the exact reference the CLI will run.
async function ensurePgmetaImage(deadline?: number) {
const expected = resolvePgmetaImage();
const resolved = await ensureImage(dockerfileServiceImage("pgmeta"), deadline);
if (resolved !== expected) {
await expectDockerSucceeded(["tag", resolved, expected], 30_000);
}
}

async function startLocalPostgres(input: { readonly projectId: string; readonly dbPort: number }) {
const containerName = localDbContainerId(input.projectId);
const networkName = localNetworkId(input.projectId);
// One shared window (already counted in the local test's timeout), with
// pg-meta's slice reserved up front: Postgres may spend the window only up
// to the point that still leaves pg-meta the default budget.
// One shared window, already counted in the local test's timeout.
const imageDeadline = resolveDeadline(LOCAL_IMAGE_BUDGET_MS);
const postgresImage = await ensureImage(LOCAL_POSTGRES_IMAGE, imageDeadline - RESOLVE_BUDGET_MS);
await ensurePgmetaImage(imageDeadline);

await expectDockerSucceeded(["network", "create", networkName], 30_000);
await expectDockerSucceeded(
Expand Down Expand Up @@ -404,8 +389,6 @@ describe("legacy gen types e2e", () => {
);
}

await ensurePgmetaImage();

for (const lang of TYPEGEN_LANGS) {
const result = await runSupabase(
["gen", "types", "--project-id", remoteProjectRef, "--lang", lang, "--schema", "public"],
Expand Down
22 changes: 18 additions & 4 deletions apps/cli/src/legacy/commands/gen/types/types.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,26 @@ export class LegacyInvalidGenTypesDurationError extends Data.TaggedError(
}
}

export class LegacyInvalidGenTypesDatabaseUrlError extends Data.TaggedError(
"LegacyInvalidGenTypesDatabaseUrlError",
)<{
/**
* A `postgrest-typegen` introspection query failed against a live database
* the CLI successfully connected to. Schema-derived, so a database finding.
*/
export class LegacyGenTypesMetadataError extends Data.TaggedError("LegacyGenTypesMetadataError")<{
readonly message: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.provideFlags;
return actionability.dbFinding;
Comment thread
avallete marked this conversation as resolved.
}
}

/**
* Language generation or formatting failed after introspection succeeded —
* a CLI packaging / formatter / template defect, not a user schema finding.
*/
export class LegacyGenTypesGenerateError extends Data.TaggedError("LegacyGenTypesGenerateError")<{
readonly message: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.internalPanic;
}
}
154 changes: 154 additions & 0 deletions apps/cli/src/legacy/commands/gen/types/types.generator.layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
generateGo,
generatePython,
generateSwift,
generateTypescript,
sortGeneratorMetadata,
} from "@supabase/postgrest-typegen/generation";
import { introspect } from "@supabase/postgrest-typegen/introspection";
import { Duration, Effect, FileSystem, Layer, Path, Result } from "effect";

import { legacyAcquirePgPool } from "../../../shared/legacy-db-connection.sql-pg.layer.ts";
import { LEGACY_PG_DELTA_CA_BUNDLE } from "../../../shared/legacy-pgdelta-ssl.ts";
import { LegacyPgDeltaSslProbe } from "../../../shared/legacy-pgdelta-ssl-probe.service.ts";
import { LegacyGenTypesGenerateError, LegacyGenTypesMetadataError } from "./types.errors.ts";
import { type LegacyGenTypesGenerateInput, LegacyGenTypesGenerator } from "./types.generator.ts";
import { legacyOxfmtTypegenFormat } from "./types.oxfmt.ts";
import { applyProbedSslMode, applyQueryTimeouts } from "./types.shared.ts";

function describeCause(cause: unknown): string {
return cause instanceof Error ? cause.message : String(cause);
}

const pinProbedCaBundle = (fs: FileSystem.FileSystem, path: Path.Path) =>
Effect.gen(function* () {
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-gen-types-ca-" });
const caPath = path.join(dir, "root.crt");
yield* fs.writeFileString(caPath, LEGACY_PG_DELTA_CA_BUNDLE);
return caPath;
}).pipe(
Effect.mapError(
(cause) =>
new LegacyGenTypesGenerateError({
message: `failed to write TLS CA bundle: ${describeCause(cause)}`,
}),
),
);

const generate = (
sslProbe: LegacyPgDeltaSslProbe["Service"],
fs: FileSystem.FileSystem,
path: Path.Path,
input: LegacyGenTypesGenerateInput,
) =>
Effect.scoped(
Effect.gen(function* () {
let conn = applyQueryTimeouts(input.conn, input.queryTimeoutSeconds);
// The driver requires TLS for remote targets, but the retired pg-meta
// path adapted to the server: its SSLRequest probe decided whether the
// container connected with TLS at all, so a plain-TCP server (common
// for self-hosted databases) still worked. Keep that adaptivity: when
// the DSN carries no explicit `sslmode`, probe the server and disable
// TLS only when it does not speak SSL. A TLS server gets the same CA
// pin pg-meta received via `PG_META_DB_SSL_ROOT_CERT`. A probe failure
// keeps the driver's TLS default so the real connect error (and its
// IPv6 pooler classification) surfaces from the connection attempt.
if (!input.isLocal && conn.sslmode === undefined) {
const probed = yield* sslProbe.requireSslForHost(conn.host, conn.port).pipe(Effect.result);
if (Result.isSuccess(probed)) {
if (!probed.success) {
conn = applyProbedSslMode(conn, false);
} else {
const sslrootcert = yield* pinProbedCaBundle(fs, path);
conn = applyProbedSslMode(conn, true, sslrootcert);
}
}
}

const pool = yield* legacyAcquirePgPool(conn, {
isLocal: input.isLocal,
dnsResolver: input.dnsResolver,
});

// `introspect` drives the injected queryable itself, so the foreign
// Promise boundary is wrapped exactly once here; a live `pg.Pool`
// satisfies its `Queryable` contract directly. `statement_timeout`
// only bounds server-side execution — also cap the client wait so a
// stalled network cannot hang past `--query-timeout`.
const introspectEffect = Effect.tryPromise({
try: () =>
introspect(
pool,
input.includedSchemas.length > 0 ? { includedSchemas: [...input.includedSchemas] } : {},
),
catch: (cause) =>
new LegacyGenTypesMetadataError({
message: `failed to introspect database: ${describeCause(cause)}`,
}),
});
const metadata =
input.queryTimeoutSeconds > 0
? yield* introspectEffect.pipe(
Effect.timeoutOrElse({
duration: Duration.seconds(input.queryTimeoutSeconds),
Comment on lines +91 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel the database operation when timing out

When an established connection stalls during introspection, this timeout interrupts only the Effect.tryPromise waiter; it does not cancel the underlying pool.query. The fresh evidence after the earlier review is that scope cleanup calls pool.end() through legacyAcquireProbedPool, but an active checked-out query prevents pool.end() from completing, and its one-second timeoutOption neither destroys the client nor closes its socket. Consequently the timeout error may be produced while the live socket still keeps the CLI process running indefinitely; make the foreign boundary cancellable by destroying/cancelling the owned query or pool when this timeout fires.

AGENTS.md reference: AGENTS.md:L92-L97

Useful? React with 👍 / 👎.

orElse: () =>
Effect.fail(
new LegacyGenTypesMetadataError({
message: `introspection exceeded --query-timeout ${input.queryTimeoutSeconds}s`,
}),
),
}),
)
: yield* introspectEffect;

// Canonical sort before generation so output is deterministic regardless
// of the introspection queries' heap order.
const sorted = sortGeneratorMetadata(metadata);

const generateError = (cause: unknown) =>
new LegacyGenTypesGenerateError({
message: `failed to generate ${input.lang} types: ${describeCause(cause)}`,
});

switch (input.lang) {
case "typescript":
return yield* Effect.tryPromise({
try: () =>
generateTypescript(sorted, {
detectOneToOneRelationships: !input.postgrestV9Compat,
// The statically-embedded oxfmt binding (see types.oxfmt.ts);
// the package's own default formatter cannot load its native
// addon inside the compiled binary.
format: legacyOxfmtTypegenFormat,
}),
catch: generateError,
});
case "go":
return yield* Effect.try({ try: () => generateGo(sorted), catch: generateError });
case "python":
return yield* Effect.try({ try: () => generatePython(sorted), catch: generateError });
case "swift":
return yield* Effect.try({
try: () => generateSwift(sorted, { accessControl: input.swiftAccessControl }),
catch: generateError,
});
}
}),
);

/**
* Production `LegacyGenTypesGenerator`: a scoped `pg.Pool` with the shared
* driver-layer connection parity (TLS mode, DoH resolver, fallback hosts),
* introspected and rendered by `@supabase/postgrest-typegen`.
*/
export const legacyGenTypesGeneratorLayer = Layer.effect(
LegacyGenTypesGenerator,
Effect.gen(function* () {
const sslProbe = yield* LegacyPgDeltaSslProbe;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
return {
generate: (input: LegacyGenTypesGenerateInput) => generate(sslProbe, fs, path, input),
};
}),
);
Loading
Loading