diff --git a/apps/cli/package.json b/apps/cli/package.json index faa2374194..0fd2023d36 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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", @@ -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:", diff --git a/apps/cli/scripts/build-binary.ts b/apps/cli/scripts/build-binary.ts index 024ee6b882..e8835b2042 100644 --- a/apps/cli/scripts/build-binary.ts +++ b/apps/cli/scripts/build-binary.ts @@ -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 @@ -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}`; diff --git a/apps/cli/scripts/build.ts b/apps/cli/scripts/build.ts index b7a2057e48..336b7141cf 100644 --- a/apps/cli/scripts/build.ts +++ b/apps/cli/scripts/build.ts @@ -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 = [ @@ -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.`); @@ -301,6 +303,7 @@ async function buildMuslBinaries() { `--define=SUPABASE_LIBC=${JSON.stringify(libc)}`, serveMainTemplateDefine, ...posthogBuildDefines, + ...oxfmtExternalArgs, `--outfile=${outfile}`, ]); diff --git a/apps/cli/scripts/bundle-externals.ts b/apps/cli/scripts/bundle-externals.ts new file mode 100644 index 0000000000..39816fd353 --- /dev/null +++ b/apps/cli/scripts/bundle-externals.ts @@ -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}`, +); 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 3875937f18..1a59a48d14 100644 --- a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md @@ -2,23 +2,23 @@ ## Files Read -| Path | Format | When | -| ----------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` or `--project-id` | -| `/supabase/config.toml` | TOML | when selecting schemas; `--local` uses embedded defaults when the file is missing | -| `{/supabase}/.env*` | dotenv | `--local`; resolves the same nested environment overrides as the legacy CLI | -| `/supabase/.temp/rest-version` | plain text | `--local` only, when `db.major_version > 14` — forces v9 compat if the tag contains `v9` | -| `/supabase/.temp/pgmeta-version` | plain text | `--local` only — overrides the pg-meta docker image tag | +| Path | Format | When | +| --------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` or `--project-id` | +| `/supabase/config.toml` | TOML | when selecting schemas; `--local` uses embedded defaults when the file is missing | +| `{/supabase}/.env*` | dotenv | `--local` (nested env overrides) and `--db-url` (shared resolver layers project env under shell `PG*` fallbacks before parsing the DSN) | +| `/supabase/.temp/rest-version` | plain text | `--local` only, when `db.major_version > 14` — forces v9 compat if the tag contains `v9` | +| `.pgpass` / `pg_service.conf` | libpq | `--db-url` only, when the DSN, `PGPASSFILE`/`PGSERVICEFILE`, or libpq defaults reference them | +| `$PGSSLROOTCERT` CA bundle | PEM | `--db-url` only, when the DSN or `PGSSLROOTCERT` sets `sslrootcert` | +| `$PGSSLCERT` / `$PGSSLKEY` | PEM | `--db-url` only, when the DSN or `PGSSLCERT`/`PGSSLKEY` set a client cert pair | ## Files Written -| Path | Format | When | -| ---- | ------ | ---- | -| — | — | — | +| Path | Format | When | +| ------------------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `$TMPDIR/supabase-gen-types-ca-*/root.crt` | PEM | remote native generation whose DSN has no explicit `sslmode` and the SSL probe reports TLS — scoped temp file of the embedded Supabase CA bundle, removed when generation finishes | -No files are written. Container env (including the DB URL and TLS CA bundle) is -passed via container CLI `run --env KEY=VALUE` arguments; no temporary env-file -is created. +No project files are written. Container env is not used. ## API Routes @@ -35,43 +35,72 @@ linked-project fallback when `--lang=typescript`. For other languages on those project-ref paths — a sanctioned intentional divergence, see Notes (CLI-1988) — the project endpoint is probed first: a `404` means the ref is a preview branch (any 404 body), so the branch endpoint supplies the branch database -host/port and credentials for pg-meta. Otherwise the database connection is resolved -for the ref and the login-role endpoint supplies temporary credentials for pg-meta. +host/port and credentials for native generation. Otherwise the database connection +is resolved for the ref and the login-role endpoint supplies temporary credentials. On an IPv4-only network where the direct database host is unreachable, project-ref -pg-meta generation retries once through the IPv4 pooler only when the current target +generation retries once through the IPv4 pooler only when the current target host is the project's direct `db.` host and the pooler URL matches the expected tenant and pooler domain. An explicit `--project-id` ref fetches the primary pooler config for that ref to build the fallback connection (the saved workdir `.temp/pooler-url` is ignored because the ref may differ from the linked workdir). `--local` and `--db-url` do not call the Management API. +## Database Access + +Except for the project-ref TypeScript path (Management API), types are generated +in-process by `@supabase/postgrest-typegen`: the CLI opens a direct Postgres +connection to the target database (the shared driver layer handles TLS for +remote targets and the `--dns-resolver` DoH mode), runs the package's +introspection queries against `pg_catalog`/`information_schema`, and renders the +requested language locally. `--query-timeout` is applied as the session's +`statement_timeout` (the flag wins over a DSN `statement_timeout`) and as a +client-side bound around `introspect()`; `0` disables both. When the connection +string carries no explicit `connect_timeout`, a positive `--query-timeout` is +also used as the connect timeout — `0` leaves the driver's default (10s remote, +2s local). `--local` connects to the host-mapped database port from +`supabase/config.toml` (`db.port`). + +For a remote target whose DSN carries no explicit `sslmode`, a raw TCP +`SSLRequest` probe (the shared pg-delta probe, default 10s timeout) is opened +to the target host/port first: a server that does not speak SSL is connected +with `sslmode=disable`, so plain-TCP databases (common when self-hosting) +keep working as they did with pg-meta. A server that speaks TLS is connected +with `sslmode=require` plus the embedded Supabase CA bundle (the driver +promotes `require` + a root cert to `verify-ca`), matching the retired +`PG_META_DB_SSL_ROOT_CERT` injection. A probe failure keeps the driver's TLS +default and lets the connection attempt surface the real error. An explicit +`sslmode` on the DSN skips the probe entirely. If `sslmode` is omitted, a +successful TLS probe replaces any DSN/`PGSSLROOTCERT` `sslrootcert` with the +embedded bundle. + +`--network-id` / `SUPABASE_NETWORK_ID` are unused: generation no longer runs +inside a container, so a hostname reachable only on a Docker network will not +resolve. `--local` uses the published host port instead; `--db-url` must be +host-reachable. + ## Subprocesses -| Command | When | Purpose | -| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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. Always passes `node dist/server/server.js` after the image. Under `SUPABASE_USE_SLIM_IMAGES`, a current Dockerfile pin may resolve to slim `ghcr.io/supabase/cli/pgmeta`; a historical `.temp/pgmeta-version` pin stays on docker.io. | +| Command | When | Purpose | +| ------------------------------------------------------------ | --------- | ---------------------------------- | +| `docker`/`podman container inspect supabase_db_` | `--local` | assert `supabase start` is running | -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. +Type generation itself runs no subprocess and pulls no container image. ## Environment Variables -| 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`) | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | pg-meta image registry override (`docker.io` → Docker Hub; any other value → that registry) | no (defaults to the ECR registry) | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the current Dockerfile pg-meta pin from the slim `ghcr.io/supabase/cli/pgmeta` build (`true`/`1` enable); a historical `.temp/pgmeta-version` pin stays on docker.io | no | -| `SUPABASE_CA_SKIP_VERIFY` | when `true`, prints a TLS-verification-disabled warning to stderr | no | +| 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 project ID | no (falls back to the workdir name) | +| `SUPABASE_DB_PORT` | local database 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 database connection | no (defaults to `127.0.0.1`) | +| `SUPABASE_NETWORK_ID` | unused (native generation does not join a Docker network) | no | +| libpq vars (`PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGSSLMODE`, `PGSSLROOTCERT`, `PGSSLCERT`, `PGSSLKEY`, `PGSSLPASSWORD`, `PGCONNECT_TIMEOUT`, `PGSERVICE`, `PGSERVICEFILE`, `PGPASSFILE`, `PGAPPNAME`, …) | `--db-url` connection fallbacks, service/passfile, and TLS files | no | ## Exit Codes @@ -83,14 +112,16 @@ timeout. | `1` | `--postgrest-v9-compat` used without `--db-url` | | `1` | invalid `--query-timeout` duration or invalid `--db-url` | | `1` | `supabase start` not running (`--local`) or db inspection failed | -| `1` | API error, TLS probe failure, or pg-meta container non-zero exit | +| `1` | API error, connection failure, or introspection/generation error | ## Output ### `--output-format text` -Prints generated TypeScript (or other language) type definitions to stdout. -Diagnostics (`Connecting to …`, pg-meta logs) go to stderr. +Prints generated TypeScript (or other language) type definitions to stdout, +followed by a single trailing newline (the same shape the retired pg-meta +container produced via `console.log`). Diagnostics (`Connecting to …`) go to +stderr. ### `--output-format json` @@ -111,18 +142,18 @@ Not applicable. - **Sanctioned intentional divergence (CLI-1988 parity ruling):** `--lang` accepts `typescript` (default), `go`, `swift`, or `python`. 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 + for TypeScript, and generate natively against the project database (temporary login-role credentials, preview-branch fallback) for the other languages. The old Go CLI instead hard-errored with `Unable to generate types for selected project. -Try using --db-url flag instead.` and never ran pg-meta for a project ref. This +Try using --db-url flag instead.` and never generated from a project ref. This permissiveness is deliberate — it resolves the user-filed CLI-1623 complaint — and was blessed in the CLI-1988 ruling; do not revert it to a hard error. The mutex groups only block `--swift-access-control` / `--query-timeout` when `--linked`/`--project-id` is - passed _explicitly_ on the command line — that combination still always runs pg-meta - with defaults (`internal` access control, one-to-one detection on, 15s timeout). On the + passed _explicitly_ on the command line — that combination still always generates with + defaults (`internal` access control, one-to-one detection on, 15s timeout). On the **implicit** linked fallback (none of `--local`/`--linked`/`--project-id`/`--db-url` passed), neither mutex key is set, so `--swift-access-control public` / - `--query-timeout 20s` clear every guard and ARE forwarded to pg-meta for `--lang + `--query-timeout 20s` clear every guard and ARE honored for `--lang go`/`--lang swift`/`--lang python` — the defaults-only claim above holds only for the explicit `--linked`/`--project-id` paths. `--postgrest-v9-compat` is unaffected by this corner: its own gate requires `--db-url` regardless of how the project ref is resolved, @@ -131,19 +162,29 @@ go`/`--lang swift`/`--lang python` — the defaults-only claim above holds only - `--schema` / `-s` accepts a comma-separated list of schemas to include. - `--swift-access-control` accepts `internal` (default) or `public`. It is mutually exclusive with an _explicit_ `--linked`/`--project-id`; on the `--local`, - `--db-url`, and implicit-linked-fallback paths it is always forwarded to pg-meta - regardless of `--lang`. -- `--postgrest-v9-compat` generates types compatible with PostgREST v9 and below. + `--db-url`, and implicit-linked-fallback paths it is always forwarded to the + generator regardless of `--lang`. +- `--postgrest-v9-compat` generates types compatible with PostgREST v9 and below + (one-to-one relationship detection disabled in the TypeScript generator). It must be used together with `--db-url` (error: `--postgrest-v9-compat must used together with --db-url` — note the typo, preserved intentionally). `--local` still forces v9 compat when the local PostgREST image tag contains `v9`. -- `--query-timeout` sets the maximum timeout for pg-meta database queries (default 15s). - It is mutually exclusive with an _explicit_ `--linked`/`--project-id`; on - the implicit linked fallback it is accepted, and forwarded to pg-meta for +- `--query-timeout` sets the maximum timeout for the introspection queries (default + 15s). It is mutually exclusive with an _explicit_ `--linked`/`--project-id`; on + the implicit linked fallback it is accepted, and honored for `--lang go`/`--lang swift`/`--lang python` (silently unused only for the implicit - linked TypeScript case, since that path never runs pg-meta). + linked TypeScript case, since that path never opens a database connection). +- `--db-url` is parsed by the shared connection resolver (libpq keywords, `PG*` env + fallbacks, `options=reference=` pooler tenants, `sslmode`), matching every + other `--db-url` command. - The legacy positional language argument (`supabase gen types typescript`) is still accepted; any other positional language requires an explicit `--lang` flag. +- Go and Python output now lists entities in the canonical sorted order + (`sortGeneratorMetadata`) instead of pg-meta's environment-dependent SQL row + order; the rendered content is otherwise identical (Swift verified + byte-identical — its template sorts internally). TypeScript is formatted by + oxfmt (postgrest-typegen ≥ 0.2.0) instead of pg-meta's prettier: content is + identical, with minor whitespace differences in how long union types wrap. - The linked-project telemetry cache is written only when a project ref is resolved (`--linked`/`--project-id`/fallback) — it's skipped when no ref is available. diff --git a/apps/cli/src/legacy/commands/gen/types/templates/prod-ca-2021.ts b/apps/cli/src/legacy/commands/gen/types/templates/prod-ca-2021.ts deleted file mode 100644 index bb81be6980..0000000000 --- a/apps/cli/src/legacy/commands/gen/types/templates/prod-ca-2021.ts +++ /dev/null @@ -1,24 +0,0 @@ -export default `-----BEGIN CERTIFICATE----- -MIIDxDCCAqygAwIBAgIUbLxMod62P2ktCiAkxnKJwtE9VPYwDQYJKoZIhvcNAQEL -BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l -dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh -c2UgUm9vdCAyMDIxIENBMB4XDTIxMDQyODEwNTY1M1oXDTMxMDQyNjEwNTY1M1ow -azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD -YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug -Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQXW -QyHOB+qR2GJobCq/CBmQ40G0oDmCC3mzVnn8sv4XNeWtE5XcEL0uVih7Jo4Dkx1Q -DmGHBH1zDfgs2qXiLb6xpw/CKQPypZW1JssOTMIfQppNQ87K75Ya0p25Y3ePS2t2 -GtvHxNjUV6kjOZjEn2yWEcBdpOVCUYBVFBNMB4YBHkNRDa/+S4uywAoaTWnCJLUi -cvTlHmMw6xSQQn1UfRQHk50DMCEJ7Cy1RxrZJrkXXRP3LqQL2ijJ6F4yMfh+Gyb4 -O4XajoVj/+R4GwywKYrrS8PrSNtwxr5StlQO8zIQUSMiq26wM8mgELFlS/32Uclt -NaQ1xBRizkzpZct9DwIDAQABo2AwXjALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFKjX -uXY32CztkhImng4yJNUtaUYsMB8GA1UdIwQYMBaAFKjXuXY32CztkhImng4yJNUt -aUYsMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB8spzNn+4VU -tVxbdMaX+39Z50sc7uATmus16jmmHjhIHz+l/9GlJ5KqAMOx26mPZgfzG7oneL2b -VW+WgYUkTT3XEPFWnTp2RJwQao8/tYPXWEJDc0WVQHrpmnWOFKU/d3MqBgBm5y+6 -jB81TU/RG2rVerPDWP+1MMcNNy0491CTL5XQZ7JfDJJ9CCmXSdtTl4uUQnSuv/Qx -Cea13BX2ZgJc7Au30vihLhub52De4P/4gonKsNHYdbWjg7OWKwNv/zitGDVDB9Y2 -CMTyZKG3XEu5Ghl1LEnI3QmEKsqaCLv12BnVjbkSeZsMnevJPs1Ye6TjjJwdik5P -o/bKiIz+Fq8= ------END CERTIFICATE----- -`; diff --git a/apps/cli/src/legacy/commands/gen/types/templates/prod-ca-2025.ts b/apps/cli/src/legacy/commands/gen/types/templates/prod-ca-2025.ts deleted file mode 100644 index cbc3299123..0000000000 --- a/apps/cli/src/legacy/commands/gen/types/templates/prod-ca-2025.ts +++ /dev/null @@ -1,24 +0,0 @@ -export default `-----BEGIN CERTIFICATE----- -MIIDxzCCAq+gAwIBAgIUeX+gpfmsRW9asFkRvjyXjHxbfgcwDQYJKoZIhvcNAQEL -BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l -dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh -c2UgUm9vdCAyMDIxIENBMB4XDTI1MDkwMzA4MDEyNVoXDTM1MDkwMTA4MDEyNVow -azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD -YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug -Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Ve7 -i9UAmc7luUilELPtqzEk8nGHxg7nY0aCStr625M7+K4OPO6RUllTsHh47k1jWyzm -LXLlyYwCsYCjQp+3vn06H+F/HRUxBt6CK2B7bNng230exTunk0xFvfkX6YgHR7B3 -1B7L25Rq3PhuRFPV4hnGYRam2XBZC4UNPqoAgrhV0HOYzXXAVoTr2yaBTMnB331Z -RwOmINh7eqTCk/JRZbb6vfZOhZRAVAe9AoRLoG8aKwmeoLGwlu0UuFx6z3E+6bmA -fSNa8Lx02GEoCdPLw9IRKUFq/SgBpQUKm44H1fDwTjH2CMM0N4p0mL/6wXnNeHvt -C40MmKZ0RcVmHE5wBwIDAQABo2MwYTAdBgNVHQ4EFgQUjvEE541toZcwtXQlZlcB -YOBRTnowHwYDVR0jBBgwFoAUjvEE541toZcwtXQlZlcBYOBRTnowDwYDVR0TAQH/ -BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBACD5IcGP -XKvS9qg0CgEQPFqYavt5c7P+0xxFgiZe+xoG8fUw58yNeK2APtgGPRpxEOGfAlNx -z9HDt4gcyHEE00B3qAVDm49pqNxioFWzNqU2LGfM/HL1QmN6urR7hCOkVCJddvOc -FhFX4nZDuRfaBboDvS5HlK3Pzxddp9hvrJi2bemr8HLqYc3HzmVckgPGSLML6t+h -4LRCXSlQsDgQ1LZ4KHsl4cq7K51N6FOXQBLB5q4lMKhs0VUhCT8Pdsj12+84laCV -c22q6p2mdT9SaernCSRnWazXWisgpjv3H7Ex4S1DCYjJIwn3PUToGFv1r8YRN2/S -O19yVSxxCIf64Sg= ------END CERTIFICATE----- -`; diff --git a/apps/cli/src/legacy/commands/gen/types/templates/staging-ca-2021.ts b/apps/cli/src/legacy/commands/gen/types/templates/staging-ca-2021.ts deleted file mode 100644 index bd30915224..0000000000 --- a/apps/cli/src/legacy/commands/gen/types/templates/staging-ca-2021.ts +++ /dev/null @@ -1,24 +0,0 @@ -export default `-----BEGIN CERTIFICATE----- -MIID1DCCArygAwIBAgIUbYRdq/8/uNq8G9stMCdOFSBgA2MwDQYJKoZIhvcNAQEL -BQAwczELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l -dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEmMCQGA1UEAwwdU3VwYWJh -c2UgU3RhZ2luZyBSb290IDIwMjEgQ0EwHhcNMjEwNDI4MTAzNjEzWhcNMzEwNDI2 -MTAzNjEzWjBzMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRGVsd2FyZTETMBEGA1UE -BwwKTmV3IENhc3RsZTEVMBMGA1UECgwMU3VwYWJhc2UgSW5jMSYwJAYDVQQDDB1T -dXBhYmFzZSBTdGFnaW5nIFJvb3QgMjAyMSBDQTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAN0AKRE8a56O8LaZxiOAcHFUFnwiKUvPoXPq26Ifw+Nv+7zg -N2V5WnMZbbw24q61Os60ZUn0XmbVtuIeJ+stPHsO7qxxuL+bmPR+qU5tkDrIOyEe -YD/2u8/q6ssVv42k4XcXbhM6RVz7CkCDY0TiBm1bMtRZso3xB6E9wAjxDf43XfV5 -PAGs3JI+Zo/vyqCDlN0hHOrB/aBl01JXqQWI84Gia5ooucq4SjA1CyawBcQ2IAvG -rXuy1BouY+xM3zRuNvtfFP6rb5Mta+jCYEMh1AZ8yP8sYUWAyhxX6k9EbOb009wQ -aZljbUCh/UglGWuBxdzePavx+zPjzWXB1NyVkpkCAwEAAaNgMF4wCwYDVR0PBAQD -AgEGMB0GA1UdDgQWBBQFx+PHLf27iIo/PMfIfGqXF7Zb+DAfBgNVHSMEGDAWgBQF -x+PHLf27iIo/PMfIfGqXF7Zb+DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB -CwUAA4IBAQB/xIiz5dDqzGXjqYqXZYx4iSfSxsVayeOPDMfmaiCfSMJEUG4cUiwG -OvMPGztaUEYeip5SCvSKuAAjVkXyP7ahKR7t7lZ9mErVXyxSZoVLbOd578CuYiZk -OgT17UjPv66WMzEKEr8wGpomTYWWfEkuqt8ENdiM1Z4LNFahdKj36+jm6/a+9R8K -25VIL68DTaQpBxFWG6ixC1HRMHJ12lDhKsshIi099BVpkGibESlxPrQOdKKqBB/J -vIX+/Hb+mS4H5zYMeK2wX0onp+GBcD6X9L1UJuXMVd+BRan8RFidXL5s3++xXjQq -Nzbc6lnA69urKffvcT07YwMsY/OmHzVa ------END CERTIFICATE----- -`; diff --git a/apps/cli/src/legacy/commands/gen/types/types.e2e.test.ts b/apps/cli/src/legacy/commands/gen/types/types.e2e.test.ts index 99e7ceacf9..c8c25e82fe 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.e2e.test.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.e2e.test.ts @@ -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]; @@ -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( @@ -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"], diff --git a/apps/cli/src/legacy/commands/gen/types/types.errors.ts b/apps/cli/src/legacy/commands/gen/types/types.errors.ts index 1285e3da84..2f4ac908e7 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.errors.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.errors.ts @@ -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; + } +} + +/** + * 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; } } diff --git a/apps/cli/src/legacy/commands/gen/types/types.generator.layer.ts b/apps/cli/src/legacy/commands/gen/types/types.generator.layer.ts new file mode 100644 index 0000000000..203e289f09 --- /dev/null +++ b/apps/cli/src/legacy/commands/gen/types/types.generator.layer.ts @@ -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), + 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), + }; + }), +); diff --git a/apps/cli/src/legacy/commands/gen/types/types.generator.ts b/apps/cli/src/legacy/commands/gen/types/types.generator.ts new file mode 100644 index 0000000000..ec5894e43a --- /dev/null +++ b/apps/cli/src/legacy/commands/gen/types/types.generator.ts @@ -0,0 +1,60 @@ +import { Context, type Effect } from "effect"; + +import type { LegacyDbConnectError } from "../../../shared/legacy-db-connection.errors.ts"; +import type { LegacyPgConnInput } from "../../../shared/legacy-db-connection.service.ts"; +import type { LegacyGenTypesGenerateError, LegacyGenTypesMetadataError } from "./types.errors.ts"; + +export type LegacyGenTypesLang = "typescript" | "go" | "swift" | "python"; + +export interface LegacyGenTypesGenerateInput { + /** The database to introspect. */ + readonly conn: LegacyPgConnInput; + /** Whether `conn` targets the local stack (drives the driver's TLS mode). */ + readonly isLocal: boolean; + /** The active `--dns-resolver` value, forwarded to the driver layer. */ + readonly dnsResolver: "native" | "https"; + readonly lang: LegacyGenTypesLang; + /** Schemas to include; empty means the introspector's own default set. */ + readonly includedSchemas: ReadonlyArray; + /** + * `--postgrest-v9-compat`: disables one-to-one relationship detection in the + * TypeScript generator (ignored by the other languages), matching the + * `PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=!v9compat` env the + * retired pg-meta container path received. + */ + readonly postgrestV9Compat: boolean; + /** `--swift-access-control` (Swift generator only). */ + readonly swiftAccessControl: "internal" | "public"; + /** + * `--query-timeout` in whole seconds. Applied as session `statement_timeout`, + * a client-side bound around `introspect()`, and — when the DSN has no + * `connect_timeout` and this value is positive — the connect timeout. + * `0` disables the query bounds and leaves the driver's connect default. + */ + readonly queryTimeoutSeconds: number; +} + +interface LegacyGenTypesGeneratorShape { + /** + * Connect to `conn`, introspect it with `@supabase/postgrest-typegen`, and + * render the generated types for `lang`. The returned string is the exact + * generator output (no trailing newline added). + */ + readonly generate: ( + input: LegacyGenTypesGenerateInput, + ) => Effect.Effect< + string, + LegacyDbConnectError | LegacyGenTypesGenerateError | LegacyGenTypesMetadataError + >; +} + +/** + * Native type generation for `gen types`, backed by + * `@supabase/postgrest-typegen` over a real Postgres connection. A service so + * handler integration tests can replace the live database + generator with a + * recording fake. + */ +export class LegacyGenTypesGenerator extends Context.Service< + LegacyGenTypesGenerator, + LegacyGenTypesGeneratorShape +>()("supabase/legacy/GenTypesGenerator") {} 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 f6c2def66a..3e1d309b97 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.handler.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.handler.ts @@ -1,10 +1,7 @@ import { loadCliConfig } from "@supabase/config/effect"; import { ChildProcessSpawner } from "effect/unstable/process"; import { Effect, FileSystem, Option, Path, Stdio, Stream } from "effect"; -import { - LegacyDnsResolverFlag, - LegacyNetworkIdFlag, -} from "../../../../shared/legacy/global-flags.ts"; +import { LegacyDnsResolverFlag } from "../../../../shared/legacy/global-flags.ts"; import { Output } from "../../../../shared/output/output.service.ts"; import { cobraMutuallyExclusiveErrorMessage, @@ -19,10 +16,7 @@ import { PROJECT_NOT_LINKED_MESSAGE, } from "../../../config/legacy-project-ref.service.ts"; import { spawnContainerCli } from "../../../shared/legacy-container-cli.ts"; -import { - legacyIsIPv6ConnectivityError, - legacyIsIPv6ConnectivityErrorCause, -} from "../../../shared/legacy-connect-errors.ts"; +import { legacyIsIPv6ConnectivityErrorCause } from "../../../shared/legacy-connect-errors.ts"; 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"; @@ -32,29 +26,23 @@ import { 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"; import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts"; import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; -import { LegacyPgDeltaSslProbe } from "../../../shared/legacy-pgdelta-ssl-probe.service.ts"; import { legacyIsDirectDbHost, legacyRunWithPoolerFallback, } from "../../../shared/legacy-pooler-fallback.ts"; import type { LegacyGenTypesFlags } from "./types.command.ts"; import { LegacyGenTypesNetworkError, LegacyGenTypesUnexpectedStatusError } from "./types.errors.ts"; +import { LegacyGenTypesGenerator } from "./types.generator.ts"; import { legacyGetHostname } from "../../../shared/legacy-hostname.ts"; import { LegacyPlatformApiFactory } from "../../../auth/legacy-platform-api-factory.service.ts"; import { defaultSchemas, - buildPostgresUrl, localDbContainerId, localDbPassword, - localNetworkId, - parseDatabaseUrl, parseQueryTimeoutSeconds, - legacyRootCaBundle, - resolvePgmetaImage, } from "./types.shared.ts"; const mapProjectTypesError = mapLegacyHttpError({ @@ -135,16 +123,6 @@ const GEN_TYPES_SCAN_SPEC = { valueFlagShorthands: new Map([["s", "schema"], ...PERSISTENT_VALUE_FLAG_SHORTHANDS]), } as const; -function forwardByteStream( - stream: Stream.Stream, - write: (text: string) => Effect.Effect, -) { - const decoder = new TextDecoder(); - return Stream.runForEach(stream, (chunk) => write(decoder.decode(chunk, { stream: true }))).pipe( - Effect.andThen(write(decoder.decode())), - ); -} - function collectByteStream(stream: Stream.Stream) { const decoder = new TextDecoder(); return Stream.runFold( @@ -226,7 +204,6 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const stdio = yield* Stdio.Stdio; - const networkId = yield* LegacyNetworkIdFlag; const dnsResolver = yield* LegacyDnsResolverFlag; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const rawArgs = yield* stdio.args; @@ -234,7 +211,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le const projectRef = yield* LegacyProjectRefResolver; const linkedProjectCache = yield* LegacyLinkedProjectCache; const dbConfig = yield* LegacyDbConfigResolver; - const sslProbe = yield* LegacyPgDeltaSslProbe; + const generator = yield* LegacyGenTypesGenerator; // "Set" follows cobra's `pflag.Changed` semantics — whether the flag was // passed at all — not the resulting value: `--linked=false` still counts @@ -297,19 +274,16 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le adHocProjectRef, }; const resolved = yield* dbConfig.resolve(resolveFlags); - const conn = resolved.conn; - yield* runPgMeta({ - url: legacyToPostgresURL(conn), - host: conn.host, - port: conn.port, - probeHost: conn.host, - probePort: conn.port, - networkMode: "host", - includedSchemas: includedSchemas.join(","), + yield* runTypegen({ + conn: resolved.conn, + isLocal: resolved.isLocal, + includedSchemas, postgrestV9Compat: flags.postgrestV9Compat, poolerFallback: { - directHost: conn.host, - eligible: !resolved.isLocal && legacyIsDirectDbHost(conn.host, cliSettings.projectHost), + directHost: resolved.conn.host, + eligible: + !resolved.isLocal && + legacyIsDirectDbHost(resolved.conn.host, cliSettings.projectHost), resolve: dbConfig.resolvePoolerFallback(resolveFlags), }, }); @@ -355,20 +329,16 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le Effect.orElseSucceed(() => Option.none()), ); - yield* runPgMeta({ - url: legacyToPostgresURL({ + yield* runTypegen({ + conn: { host: branch.db_host, port: branch.db_port, user: branchUser, password: branchPassword, database: "postgres", - }), - host: branch.db_host, - port: branch.db_port, - probeHost: branch.db_host, - probePort: branch.db_port, - networkMode: "host", - includedSchemas: includedSchemas.join(","), + }, + isLocal: false, + includedSchemas, postgrestV9Compat: flags.postgrestV9Compat, poolerFallback: { directHost: branch.db_host, @@ -378,129 +348,52 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le }); }); - const runPgMeta = (input: { - readonly url: string; - readonly host: string; - readonly port: number; - readonly probeHost: string; - readonly probePort: number; - readonly networkMode: "host" | (string & {}); - readonly includedSchemas: string; + const runTypegen = (input: { + readonly conn: LegacyPgConnInput; + readonly isLocal: boolean; + readonly includedSchemas: ReadonlyArray; readonly postgrestV9Compat: boolean; - readonly pgmetaVersionOverride?: string; readonly poolerFallback?: { readonly directHost: string; readonly eligible: boolean; readonly resolve: Effect.Effect, unknown>; }; }) => - Effect.scoped( - Effect.gen(function* () { - const buildRun = (target: { - readonly url: string; - readonly host: string; - readonly port: number; - readonly probeHost: string; - readonly probePort: number; - }) => - Effect.gen(function* () { - yield* output.raw(`Connecting to ${target.host} ${target.port}\n`, "stderr"); - - // Each entry is a "KEY=VALUE" string, passed as a `--env - // KEY=VALUE` argument rather than a `--env-file`: env-files - // split on newlines, so they cannot carry the multi-line PEM CA - // bundle, and a value containing a newline could inject an extra - // variable. Passing argv elements keeps each entry as exactly - // one variable regardless of its contents. - const env = [ - `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_INCLUDED_SCHEMAS=${input.includedSchemas}`, - `PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL=${swiftAccessControl}`, - `PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=${String(!input.postgrestV9Compat)}`, - ]; - - // Emitted to stderr when the probe runs with certificate - // verification disabled. Our wire-level SSLRequest probe never - // verifies certificates, so honour the same env var here too. - if (process.env["SUPABASE_CA_SKIP_VERIFY"] === "true") { - yield* output.raw( - "WARNING: TLS certificate verification disabled for SSL probe (SUPABASE_CA_SKIP_VERIFY=true)\n", - "stderr", - ); - } - - const useTls = yield* sslProbe.requireSslForHost(target.probeHost, target.probePort); - if (useTls) { - env.push(`PG_META_DB_SSL_ROOT_CERT=${legacyRootCaBundle()}`); - } - - // `--network-id` overrides any base network mode (even the - // "host" mode used for --db-url), so honour the override here too. - const networkMode = Option.isSome(networkId) ? networkId.value : input.networkMode; - const pgmetaImage = resolvePgmetaImage(input.pgmetaVersionOverride); - const args = [ - "run", - "--rm", - "--network", - networkMode, - ...env.flatMap((entry) => ["--env", entry]), - pgmetaImage, - "node", - "dist/server/server.js", - ]; - const child = yield* spawnContainerCli(spawner, args, { - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - - let stderrText = ""; - const [exitCode] = yield* Effect.all( - [ - child.exitCode.pipe(Effect.map(Number)), - forwardByteStream(child.stdout, (text) => output.raw(text, "stdout")), - forwardByteStream(child.stderr, (text) => - Effect.sync(() => { - stderrText += text; - }).pipe(Effect.andThen(output.raw(text, "stderr"))), - ), - ], - { concurrency: "unbounded" }, - ); - return { exitCode, stderrText }; + Effect.gen(function* () { + const generateTarget = (conn: LegacyPgConnInput, isLocal: boolean) => + Effect.gen(function* () { + yield* output.raw(`Connecting to ${conn.host} ${conn.port}\n`, "stderr"); + return yield* generator.generate({ + conn, + isLocal, + dnsResolver, + lang, + includedSchemas: input.includedSchemas, + postgrestV9Compat: input.postgrestV9Compat, + swiftAccessControl, + queryTimeoutSeconds, }); + }); - const runTarget = (conn: LegacyPgConnInput) => - buildRun({ - url: legacyToPostgresURL(conn), - host: conn.host, - port: conn.port, - probeHost: conn.host, - probePort: conn.port, - }); + const types = + input.poolerFallback === undefined + ? yield* generateTarget(input.conn, input.isLocal) + : yield* legacyRunWithPoolerFallback({ + run: generateTarget(input.conn, input.isLocal), + // The pooler endpoint is always a remote target, even when the + // direct attempt was against a local-looking host. + retry: (pooler) => generateTarget(pooler, false), + directHost: input.poolerFallback.directHost, + eligible: input.poolerFallback.eligible, + resolveFallback: input.poolerFallback.resolve, + classifyError: legacyIsIPv6ConnectivityErrorCause, + }); - const result = - input.poolerFallback === undefined - ? yield* buildRun(input) - : yield* legacyRunWithPoolerFallback({ - run: buildRun(input), - retry: runTarget, - directHost: input.poolerFallback.directHost, - eligible: input.poolerFallback.eligible, - resolveFallback: input.poolerFallback.resolve, - classifyError: legacyIsIPv6ConnectivityErrorCause, - classifyResult: (result) => - result.exitCode !== 0 && legacyIsIPv6ConnectivityError(result.stderrText), - }); - - if (result.exitCode !== 0) { - return yield* Effect.fail(new Error(`error running container: exit ${result.exitCode}`)); - } - }), - ); + // The retired pg-meta container printed the generated output through + // `console.log`, so a single trailing newline is part of the + // established stdout contract. + yield* output.raw(`${types}\n`); + }); const assertLocalDbRunning = (projectId: string) => Effect.scoped( @@ -593,10 +486,10 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le ); const paths = legacyTempPaths(path, cliSettings.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. + // The local PostgREST image is resolved from the rest-version file only when + // Db.MajorVersion > 14; when that image tag contains "v9" the generated types + // must stay v9-compatible. Gate and trim identically so we don't force v9 on + // older databases. const restVersion = config.majorVersion > 14 ? (yield* fs @@ -604,49 +497,41 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le .pipe(Effect.orElseSucceed(() => ""))).trim() : ""; const forcedV9 = restVersion.length > 0 && restVersion.includes("v9"); - const pgmetaVersionOverride = yield* fs - .readFileString(paths.pgmetaVersion) - .pipe(Effect.orElseSucceed(() => "")); - const includedSchemas = ( - schemas.length > 0 ? schemas : defaultSchemas(config.apiSchemas) - ).join(","); + const includedSchemas = schemas.length > 0 ? schemas : defaultSchemas(config.apiSchemas); yield* assertLocalDbRunning(projectId); - yield* runPgMeta({ - url: buildPostgresUrl({ - host: "db", - port: 5432, + yield* runTypegen({ + conn: { + host: legacyGetHostname(), + port: config.port, user: "postgres", password: localDbPassword(), database: "postgres", - }), - host: "db", - port: 5432, - probeHost: legacyGetHostname(), - probePort: config.port, - networkMode: localNetworkId(projectId), + }, + isLocal: true, includedSchemas, postgrestV9Compat: flags.postgrestV9Compat || forcedV9, - pgmetaVersionOverride, }); return; } if (Option.isSome(flags.dbUrl)) { const loaded = yield* loadConfig(); - const direct = yield* parseDatabaseUrl(flags.dbUrl.value); - const includedSchemas = ( - schemas.length > 0 ? schemas : defaultSchemas(loaded?.config.api.schemas ?? []) - ).join(","); - - yield* runPgMeta({ - url: direct.url, - host: direct.host, - port: direct.port, - probeHost: direct.host, - probePort: direct.port, - networkMode: direct.networkMode, + const includedSchemas = + schemas.length > 0 ? schemas : defaultSchemas(loaded?.config.api.schemas ?? []); + // The shared resolver parses the DSN pgconn-style (libpq keywords, + // `options=reference=…` pooler tenants, sslmode, PG* env fallbacks) and + // detects a local target, matching every other `--db-url` command. + const resolved = yield* dbConfig.resolve({ + dbUrl: flags.dbUrl, + connType: "db-url", + dnsResolver, + }); + + yield* runTypegen({ + conn: resolved.conn, + isLocal: resolved.isLocal, includedSchemas, postgrestV9Compat: flags.postgrestV9Compat, }); 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 a8f8f33e2f..085241e401 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 @@ -1,5 +1,4 @@ import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; @@ -11,28 +10,18 @@ import type { V1GetProjectOutput, } from "@supabase/api/effect"; import { ChildProcessSpawner } from "effect/unstable/process"; -import { CliOutput, Command } from "effect/unstable/cli"; import * as HttpClientError from "effect/unstable/http/HttpClientError"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; import { Deferred, Effect, Exit, Layer, Option, PlatformError, Sink, Stdio, Stream } from "effect"; import { - LEGACY_GLOBAL_FLAGS, LegacyDebugFlag, LegacyDnsResolverFlag, - LegacyNetworkIdFlag, LegacyOutputFlag, } from "../../../../shared/legacy/global-flags.ts"; import { LegacyPlatformApiFactory } from "../../../auth/legacy-platform-api-factory.service.ts"; import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts"; -import { - mockAnalytics, - mockOutput, - mockProcessControl, - mockRuntimeInfo, - mockTty, - processEnvLayer, -} from "../../../../../tests/helpers/mocks.ts"; +import { mockOutput, mockProcessControl } from "../../../../../tests/helpers/mocks.ts"; import { buildLegacyTestRuntime, LEGACY_VALID_REF, @@ -42,32 +31,21 @@ import { mockLegacyTelemetryStateTracked, } from "../../../../../tests/helpers/legacy-mocks.ts"; import { mockChildProcessSpawner } from "../../../../../../../packages/process-compose/tests/helpers/mocks.ts"; -import { textCliOutputFormatter } from "../../../../shared/output/text-formatter.ts"; -import { processControlLayer } from "../../../../shared/runtime/process-control.layer.ts"; -import { TelemetryRuntime } from "../../../../shared/telemetry/runtime.service.ts"; -import { makeTelemetryIdentity } from "../../../../shared/telemetry/identity.ts"; import type { LegacyPgConnInput } from "../../../shared/legacy-db-connection.service.ts"; +import { LegacyDbConnectError } from "../../../shared/legacy-db-connection.errors.ts"; import type { LegacyDbConfigError } from "../../../shared/legacy-db-config.service.ts"; import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; import { LegacyDbConfigLoadError } from "../../../shared/legacy-db-config.errors.ts"; -import { - LegacyPgDeltaSslProbe, - LegacyPgDeltaSslProbeError, -} from "../../../shared/legacy-pgdelta-ssl-probe.service.ts"; -import { legacyPgDeltaSslProbeLayer } from "../../../shared/legacy-pgdelta-ssl-probe.layer.ts"; import type { LegacyDbConfigFlags, LegacyResolvedDbConfig, } from "../../../shared/legacy-db-config.types.ts"; -import { legacyGenCommand } from "../gen.command.ts"; import type { LegacyGenTypesFlags } from "./types.command.ts"; +import { LegacyGenTypesMetadataError } from "./types.errors.ts"; +import type { LegacyGenTypesGenerateInput } from "./types.generator.ts"; +import { LegacyGenTypesGenerator } from "./types.generator.ts"; import { legacyGenTypes } from "./types.handler.ts"; -import { - localDbContainerId, - localNetworkId, - parseQueryTimeoutSeconds, - resolvePgmetaImage, -} from "./types.shared.ts"; +import { localDbContainerId, parseQueryTimeoutSeconds } from "./types.shared.ts"; function writeConfig(workdir: string, contents: string) { const supabaseDir = join(workdir, "supabase"); @@ -89,42 +67,6 @@ function ensureDefaultConfig(workdir: string) { writeConfig(workdir, ['project_id = "demo"', "", "[api]", "schemas = []"].join("\n")); } -/** Extracts the `KEY=VALUE` entries passed via `docker run --env ` arguments. */ -function dockerEnv(args: ReadonlyArray) { - const entries: string[] = []; - for (let index = 0; index < args.length; index += 1) { - if (args[index] === "--env") { - const entry = args[index + 1]; - if (entry !== undefined) { - entries.push(entry); - } - } - } - return { - entries, - has: (entry: string) => entries.includes(entry), - startsWith: (prefix: string) => entries.some((entry) => entry.startsWith(prefix)), - }; -} - -/** The argv of the `docker run` invocation captured during a spawn. */ -function captureDockerRun() { - let args: ReadonlyArray | undefined; - return { - onSpawn: (record: { readonly command: string; readonly args: ReadonlyArray }) => { - if (record.command === "docker" && record.args.includes("run")) { - args = record.args; - } - }, - get args() { - return args; - }, - get env() { - return dockerEnv(args ?? []); - }, - }; -} - function defaultFlags(overrides: Partial = {}): LegacyGenTypesFlags { return { local: false, @@ -200,6 +142,30 @@ function mockDbConfigResolver( return { layer, resolves, poolerFallbacks }; } +/** + * Recording fake for the native typegen seam. Each `generate` call is captured; + * the nth call resolves with the nth entry of `results` (falling back to a + * plain `"generated"` success when the list is exhausted or absent). + */ +function mockLegacyGenTypesGenerator( + opts: { + readonly output?: string; + readonly results?: ReadonlyArray< + Effect.Effect + >; + } = {}, +) { + const calls: Array = []; + const layer = Layer.succeed(LegacyGenTypesGenerator, { + generate: (input) => + Effect.suspend(() => { + calls.push(input); + return opts.results?.[calls.length - 1] ?? Effect.succeed(opts.output ?? "generated"); + }), + }); + return { layer, calls }; +} + type BranchConfig = typeof V1GetABranchConfigOutput.Type; type LoginRole = typeof V1CreateLoginRoleOutput.Type; type PoolerConfig = typeof V1GetPoolerConfigOutput.Type; @@ -213,17 +179,19 @@ function setup( readonly format?: "text" | "json" | "stream-json"; readonly goOutput?: Option.Option<"env" | "pretty" | "json" | "toml" | "yaml">; readonly projectTypes?: string; - readonly childStdout?: ReadonlyArray; readonly childStderr?: ReadonlyArray; readonly childExitCode?: number; readonly childLayer?: Layer.Layer; readonly debug?: boolean; - readonly networkId?: Option.Option; readonly onSpawn?: (record: { readonly command: string; readonly args: ReadonlyArray; }) => void; readonly args?: ReadonlyArray; + readonly generatorOutput?: string; + readonly generatorResults?: ReadonlyArray< + Effect.Effect + >; readonly generateTypescriptTypes?: (input: { readonly ref: string; readonly included_schemas?: string; @@ -244,7 +212,6 @@ function setup( ) => Effect.Effect; readonly poolerFallback?: Option.Option; readonly poolerFallbackFails?: boolean; - readonly sslProbeLayer?: Layer.Layer; } = {}, ) { const workdir = opts.workdir ?? mkdtempSync(join(tmpdir(), "supabase-gen-types-")); @@ -262,9 +229,13 @@ function setup( poolerFallback: opts.poolerFallback, poolerFallbackFails: opts.poolerFallbackFails, }); + const generator = mockLegacyGenTypesGenerator({ + output: opts.generatorOutput, + results: opts.generatorResults, + }); const processControl = mockProcessControl(); const child = mockChildProcessSpawner({ - stdout: [...(opts.childStdout ?? [])], + stdout: [], stderr: [...(opts.childStderr ?? [])], exitCode: opts.childExitCode ?? 0, onSpawn: opts.onSpawn, @@ -361,15 +332,11 @@ function setup( Layer.succeed(LegacyOutputFlag, opts.goOutput ?? Option.none()), Layer.succeed(LegacyDebugFlag, opts.debug ?? false), Layer.succeed(LegacyDnsResolverFlag, "native" as const), - Layer.succeed(LegacyNetworkIdFlag, opts.networkId ?? Option.none()), - opts.sslProbeLayer ?? - legacyPgDeltaSslProbeLayer.pipe( - Layer.provide(Layer.succeed(LegacyDebugFlag, opts.debug ?? false)), - ), Layer.succeed(LegacyPlatformApiFactory, { make: LegacyPlatformApi.pipe(Effect.provide(api.layer)), }), dbConfig.layer, + generator.layer, ); return { @@ -378,6 +345,7 @@ function setup( telemetry, linkedProjectCache, dbConfig, + generator, processControl, child, api, @@ -385,67 +353,6 @@ function setup( }; } -function mockSequentialChildProcessSpawner( - steps: ReadonlyArray<{ - readonly exitCode?: number; - readonly stdout?: ReadonlyArray; - readonly stderr?: ReadonlyArray; - }>, -) { - const encoder = new TextEncoder(); - const spawned: Array<{ command: string; args: ReadonlyArray }> = []; - let stepIndex = 0; - - const layer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make((command) => - Effect.gen(function* () { - const cmd = command._tag === "StandardCommand" ? command.command : ""; - const args = command._tag === "StandardCommand" ? command.args : []; - spawned.push({ command: cmd, args }); - - const step = steps[Math.min(stepIndex, steps.length - 1)]; - stepIndex += 1; - const exitDeferred = yield* Deferred.make(); - - yield* Effect.forkDetach( - Effect.gen(function* () { - yield* Effect.sleep("10 millis"); - yield* Deferred.succeed( - exitDeferred, - ChildProcessSpawner.ExitCode(step?.exitCode ?? 0), - ); - }), - ); - - const stdoutBytes = (step?.stdout ?? []).map((line) => encoder.encode(`${line}\n`)); - const stderrBytes = (step?.stderr ?? []).map((line) => encoder.encode(`${line}\n`)); - - return ChildProcessSpawner.makeHandle({ - pid: ChildProcessSpawner.ProcessId(2000 + spawned.length), - stdout: Stream.fromIterable(stdoutBytes), - stderr: Stream.fromIterable(stderrBytes), - all: Stream.empty, - exitCode: Deferred.await(exitDeferred), - isRunning: Effect.succeed(false), - stdin: Sink.drain, - kill: () => Effect.void, - unref: Effect.succeed(Effect.void), - getInputFd: () => Sink.drain, - getOutputFd: () => Stream.empty, - }); - }), - ), - ); - - return { - layer, - get spawned() { - return spawned; - }, - }; -} - function mockDockerMissingChildProcessSpawner( steps: ReadonlyArray<{ readonly exitCode?: number; @@ -518,54 +425,19 @@ function mockDockerMissingChildProcessSpawner( }; } -async function withSslProbeServer( - run: (port: number) => Promise, - response: "N" | "S" = "N", - options: { readonly host?: string; readonly port?: number } = {}, -): Promise { - const host = options.host ?? "127.0.0.1"; - const port = options.port ?? 0; - const server = createServer((socket) => { - socket.once("data", () => { - socket.write(Buffer.from(response)); - socket.end(); - }); - }); - - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(port, host, () => resolve()); - }); - - const address = server.address(); - if (address === null || typeof address === "string") { - server.close(); - throw new Error("failed to bind ssl probe server"); - } - - try { - return await run(address.port); - } finally { - await new Promise((resolve, reject) => - server.close((error) => (error ? reject(error) : resolve())), - ); - } -} +const IPV6_CONNECT_FAILURE = new LegacyDbConnectError({ + message: `failed to connect to postgres: could not translate host name "db.${LEGACY_VALID_REF}.supabase.co" to address: No address associated with hostname`, +}); const nonTypescriptProjectRefScenarios = [ - { lang: "go", stdout: "type PublicMovies struct {}" }, - { lang: "swift", stdout: "struct PublicMovies: Codable {}" }, - { lang: "python", stdout: "class PublicMovies(BaseModel):" }, + { lang: "go", output: "type PublicMovies struct {}" }, + { lang: "swift", output: "struct PublicMovies: Codable {}" }, + { lang: "python", output: "class PublicMovies(BaseModel):" }, ] as const satisfies ReadonlyArray<{ readonly lang: Exclude; - readonly stdout: string; + readonly output: string; }>; -const legacyTestRoot = Command.make("supabase").pipe( - Command.withSubcommands([legacyGenCommand]), - Command.withGlobalFlags(LEGACY_GLOBAL_FLAGS), -); - describe("legacy gen types", () => { it.effect("accepts Go-style microsecond duration aliases", () => Effect.gen(function* () { @@ -574,84 +446,6 @@ describe("legacy gen types", () => { }), ); - it.live("runs tokenless local generation through command wiring", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-command-local-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); - const out = mockOutput({ format: "text", interactive: false }); - const analytics = mockAnalytics(); - const child = mockSequentialChildProcessSpawner([ - { exitCode: 0 }, - { exitCode: 0, stdout: ["export type Database = {};"] }, - ]); - const args = [ - "gen", - "types", - "typescript", - "--local", - "--schema", - "public", - "--workdir", - workdir, - ]; - const layer = Layer.mergeAll( - BunServices.layer, - CliOutput.layer(textCliOutputFormatter()), - out.layer, - analytics.layer, - processControlLayer, - processEnvLayer({ SUPABASE_HOME: workdir }), - mockRuntimeInfo({ cwd: workdir, homeDir: workdir }), - mockTty({ stdinIsTty: false, stdoutIsTty: false }), - child.layer, - Stdio.layerTest({ args: Effect.succeed(args) }), - Layer.succeed( - TelemetryRuntime, - TelemetryRuntime.of({ - configDir: join(workdir, ".supabase"), - tracesDir: join(workdir, ".supabase", "traces"), - consent: "granted", - showDebug: false, - deviceId: "test-device-id", - sessionId: "test-session-id", - identity: makeTelemetryIdentity(undefined), - isFirstRun: false, - isTty: false, - isCi: false, - os: "linux", - arch: "x64", - cliVersion: "0.1.0", - }), - ), - ); - - await Effect.runPromise( - Command.runWith(legacyTestRoot, { version: "0.0.0-test" })(args).pipe( - Effect.provide(layer), - ) as Effect.Effect, - ); - - expect(out.stdoutText).toContain("export type Database = {};"); - expect(out.stderrText).not.toContain("Access token not provided"); - expect(child.spawned).toHaveLength(2); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); - it.live("generates typescript types from a project ref", () => { const { layer, out, api, linkedProjectCache, telemetry } = setup({ projectId: Option.some(LEGACY_VALID_REF), @@ -814,9 +608,8 @@ describe("legacy gen types", () => { // this argv (both `local` and `linked` parse as independently true, // since its tokenizer is unaware of pflag's value consumption); only the // pflag-faithful scan can tell them apart. - // `childExitCode: 1` fails the local target's `container inspect`, keeping the - // downstream failure deterministic before the real SSL probe can reach whatever - // is listening on the local db port. + // `childExitCode: 1` fails the local target's `container inspect`, keeping + // the downstream failure deterministic before any generation runs. const { layer } = setup({ args: ["gen", "types", "-s", "--linked", "--local"], childExitCode: 1, @@ -1038,56 +831,38 @@ describe("legacy gen types", () => { }); it.live( - "forwards --query-timeout and --swift-access-control to pg-meta for implicit linked non-TypeScript generation", - () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer, dbConfig } = setup({ - args: [ - "gen", - "types", - "--lang", - "go", - "--query-timeout", - "20s", - "--swift-access-control", - "public", - ], - projectId: Option.some(LEGACY_VALID_REF), - childStdout: ["type PublicMovies struct {}"], - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: "127.0.0.1", - port, - user: "postgres", - password: "workdir-password", - database: "postgres", - }), - ), - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ lang: "go", queryTimeout: "20s", swiftAccessControl: "public" }), - ).pipe(Effect.provide(layer)), - ); + "forwards --query-timeout and --swift-access-control to the generator for implicit linked non-TypeScript generation", + () => { + const { layer, dbConfig, generator } = setup({ + args: [ + "gen", + "types", + "--lang", + "go", + "--query-timeout", + "20s", + "--swift-access-control", + "public", + ], + projectId: Option.some(LEGACY_VALID_REF), + generatorOutput: "type PublicMovies struct {}", + }); - // Unlike an explicit --linked/--project-id, the implicit fallback never - // sets the "linked"/"project-id" mutex keys, so --query-timeout and - // --swift-access-control clear every guard here and reach pg-meta — the - // SIDE_EFFECTS.md defaults-invariant note is scoped to the explicit - // paths for exactly this reason. - expect(dbConfig.resolves[0]?.adHocProjectRef).toBe(false); - expect(docker.env.has("PG_QUERY_TIMEOUT_SECS=20")).toBe(true); - expect(docker.env.has("PG_CONN_TIMEOUT_SECS=20")).toBe(true); - expect(docker.env.has("PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL=public")).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ lang: "go", queryTimeout: "20s", swiftAccessControl: "public" }), + ).pipe(Effect.provide(layer)); + + // Unlike an explicit --linked/--project-id, the implicit fallback never + // sets the "linked"/"project-id" mutex keys, so --query-timeout and + // --swift-access-control clear every guard here and reach the + // generator — the SIDE_EFFECTS.md defaults-invariant note is scoped to + // the explicit paths for exactly this reason. + expect(dbConfig.resolves[0]?.adHocProjectRef).toBe(false); + expect(generator.calls[0]?.queryTimeoutSeconds).toBe(20); + expect(generator.calls[0]?.swiftAccessControl).toBe("public"); + }); + }, ); it.live("prefers the --postgrest-v9-compat guard over mutex group errors", () => { @@ -1164,1113 +939,710 @@ describe("legacy gen types", () => { }); }); - it.live("allows --swift-access-control for local non-Swift generation", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-swift-flag-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); + it.live("allows --swift-access-control for local non-Swift generation", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-swift-flag-")); + writeConfig( + workdir, + ['project_id = "demo"', "", "[api]", 'schemas = ["public"]', "", "[db]", "port = 54321"].join( + "\n", + ), + ); - const { layer } = setup({ - workdir, - args: [ - "gen", - "types", - "--local", - "--lang", - "python", - "--swift-access-control", - "public", - ], - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - // Go has no "--swift-access-control requires --lang swift" guard — - // the value is always forwarded to pg-meta regardless of language. - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ local: true, lang: "python", swiftAccessControl: "public" }), - ).pipe(Effect.provide(layer)), - ); + const { layer, generator } = setup({ + workdir, + args: ["gen", "types", "--local", "--lang", "python", "--swift-access-control", "public"], + }); - expect(docker.env.has("PG_META_GENERATE_TYPES=python")).toBe(true); - expect(docker.env.has("PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL=public")).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + // Go has no "--swift-access-control requires --lang swift" guard — + // the value is always forwarded to the generator regardless of language. + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ local: true, lang: "python", swiftAccessControl: "public" }), + ).pipe(Effect.provide(layer)); - it.live("allows --postgrest-v9-compat together with --db-url", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer } = setup({ - args: [ - "gen", - "types", - "--db-url", - `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`, - "--postgrest-v9-compat", - ], - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - postgrestV9Compat: true, - }), - ).pipe(Effect.provide(layer)), - ); + expect(generator.calls[0]?.lang).toBe("python"); + expect(generator.calls[0]?.swiftAccessControl).toBe("public"); + }); + }); - expect( - docker.env.has("PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=false"), - ).toBe(true); + it.live("allows --postgrest-v9-compat together with --db-url", () => { + const dbUrl = "postgresql://postgres:postgres@127.0.0.1:5432/postgres"; + const { layer, generator } = setup({ + args: ["gen", "types", "--db-url", dbUrl, "--postgrest-v9-compat"], + }); + + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(dbUrl), + postgrestV9Compat: true, }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + ).pipe(Effect.provide(layer)); + + expect(generator.calls[0]?.postgrestV9Compat).toBe(true); + }); + }); for (const scenario of nonTypescriptProjectRefScenarios) { - it.live(`generates ${scenario.lang} types from a project ref through the DB resolver`, () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer, out, child, api, linkedProjectCache, dbConfig } = setup({ - args: ["gen", "types", "--lang", scenario.lang, "--project-id", LEGACY_VALID_REF], - childStdout: [scenario.stdout], - dbConfigResolve: (input) => - Effect.succeed( - remoteResolvedConfig( - { - host: "127.0.0.1", - port, - user: `cli_login_${LEGACY_VALID_REF}`, - password: "temporary-password", - database: "postgres", - }, - (input.linkedProjectRef !== undefined - ? Option.getOrUndefined(input.linkedProjectRef) - : undefined) ?? LEGACY_VALID_REF, - ), - ), - getABranchConfig: ({ branch_id_or_ref }) => - Effect.fail(new Error(`unexpected preview branch lookup for ${branch_id_or_ref}`)), - getProject: ({ ref }) => - Effect.succeed({ - id: ref, - ref, - organization_id: "org-id", - organization_slug: "org", - name: "demo", - region: "us-east-1", - created_at: "2025-01-01T00:00:00Z", - status: "ACTIVE_HEALTHY", - database: { - host: `127.0.0.1:${port}`, - version: "15.1", - postgres_engine: "15", - release_channel: "ga", - }, - }), - createLoginRole: ({ ref }) => - Effect.fail(new Error(`unexpected login role creation for ${ref}`)), - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: scenario.lang, - }), - ).pipe(Effect.provide(layer)), - ); + it.live(`generates ${scenario.lang} types from a project ref through the DB resolver`, () => { + const { layer, out, api, linkedProjectCache, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", scenario.lang, "--project-id", LEGACY_VALID_REF], + generatorOutput: scenario.output, + dbConfigResolve: (input) => + Effect.succeed( + remoteResolvedConfig( + { + host: "127.0.0.1", + port: 5432, + user: `cli_login_${LEGACY_VALID_REF}`, + password: "temporary-password", + database: "postgres", + }, + (input.linkedProjectRef !== undefined + ? Option.getOrUndefined(input.linkedProjectRef) + : undefined) ?? LEGACY_VALID_REF, + ), + ), + getABranchConfig: ({ branch_id_or_ref }) => + Effect.fail(new Error(`unexpected preview branch lookup for ${branch_id_or_ref}`)), + createLoginRole: ({ ref }) => + Effect.fail(new Error(`unexpected login role creation for ${ref}`)), + }); - expect(api.requests).toContainEqual({ - method: "getProject", - input: { ref: LEGACY_VALID_REF }, - }); - expect(api.requests).not.toContainEqual( - expect.objectContaining({ method: "createLoginRole" }), - ); - expect(api.requests).not.toContainEqual( - expect.objectContaining({ method: "getABranchConfig" }), - ); - expect(api.requests).not.toContainEqual( - expect.objectContaining({ method: "generateTypescriptTypes" }), - ); - expect(child.spawned[0]?.args).toContain("--network"); - expect(child.spawned[0]?.args).toContain("host"); - expect(out.stderrText).toContain(`Connecting to 127.0.0.1 ${port}`); - expect( - docker.env.has( - `PG_META_DB_URL=postgresql://cli_login_${LEGACY_VALID_REF}:temporary-password@127.0.0.1:${port}/postgres?connect_timeout=10`, - ), - ).toBe(true); - expect(dbConfig.resolves).toHaveLength(1); - expect(dbConfig.resolves[0]?.connType).toBe("linked"); - // --project-id is an ad-hoc remote ref: the resolver must not inherit - // the workdir's ambient password / saved pooler URL. - expect(dbConfig.resolves[0]?.adHocProjectRef).toBe(true); - const linkedProjectRef = dbConfig.resolves[0]?.linkedProjectRef; - expect( - linkedProjectRef !== undefined ? Option.getOrUndefined(linkedProjectRef) : undefined, - ).toBe(LEGACY_VALID_REF); - expect(docker.env.has(`PG_META_GENERATE_TYPES=${scenario.lang}`)).toBe(true); - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public")).toBe(true); - expect(out.stdoutText).toContain(scenario.stdout); - expect(linkedProjectCache.cached).toBe(true); + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: scenario.lang, }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); - } + ).pipe(Effect.provide(layer)); - it.live("resolves the linked workdir DB without ad-hoc project-ref semantics", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--linked"], - projectId: Option.some(LEGACY_VALID_REF), - childStdout: ["type PublicMovies struct {}"], - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: "127.0.0.1", - port, - user: "postgres", - password: "workdir-password", - database: "postgres", - }), - ), - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes(defaultFlags({ linked: true, lang: "go" })).pipe(Effect.provide(layer)), - ); + expect(api.requests).toContainEqual({ + method: "getProject", + input: { ref: LEGACY_VALID_REF }, + }); + expect(api.requests).not.toContainEqual( + expect.objectContaining({ method: "createLoginRole" }), + ); + expect(api.requests).not.toContainEqual( + expect.objectContaining({ method: "getABranchConfig" }), + ); + expect(api.requests).not.toContainEqual( + expect.objectContaining({ method: "generateTypescriptTypes" }), + ); + expect(out.stderrText).toContain("Connecting to 127.0.0.1 5432"); + expect(dbConfig.resolves).toHaveLength(1); + expect(dbConfig.resolves[0]?.connType).toBe("linked"); + // --project-id is an ad-hoc remote ref: the resolver must not inherit + // the workdir's ambient password / saved pooler URL. + expect(dbConfig.resolves[0]?.adHocProjectRef).toBe(true); + const linkedProjectRef = dbConfig.resolves[0]?.linkedProjectRef; + expect( + linkedProjectRef !== undefined ? Option.getOrUndefined(linkedProjectRef) : undefined, + ).toBe(LEGACY_VALID_REF); + expect(generator.calls).toHaveLength(1); + expect(generator.calls[0]?.conn).toEqual({ + host: "127.0.0.1", + port: 5432, + user: `cli_login_${LEGACY_VALID_REF}`, + password: "temporary-password", + database: "postgres", + }); + expect(generator.calls[0]?.isLocal).toBe(false); + expect(generator.calls[0]?.lang).toBe(scenario.lang); + expect(generator.calls[0]?.includedSchemas).toEqual(["public"]); + expect(out.stdoutText).toBe(`${scenario.output}\n`); + expect(linkedProjectCache.cached).toBe(true); + }); + }); + } - expect(dbConfig.resolves).toHaveLength(1); - expect(dbConfig.resolves[0]?.connType).toBe("linked"); - // --linked is the workdir's own project: keep workdir-scoped credentials. - expect(dbConfig.resolves[0]?.adHocProjectRef).toBe(false); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + it.live("resolves the linked workdir DB without ad-hoc project-ref semantics", () => { + const { layer, dbConfig } = setup({ + args: ["gen", "types", "--lang", "go", "--linked"], + projectId: Option.some(LEGACY_VALID_REF), + generatorOutput: "type PublicMovies struct {}", + }); - it.live("preserves resolver URL options for remote non-TypeScript typegen", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childStdout: ["type PublicMovies struct {}"], - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: "127.0.0.1", - port, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", - database: "postgres", - options: `reference=${LEGACY_VALID_REF}`, - }), - ), - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ linked: true, lang: "go" })).pipe(Effect.provide(layer)); - expect( - docker.env.has( - `PG_META_DB_URL=postgresql://postgres.${LEGACY_VALID_REF}:pooler-password@127.0.0.1:${port}/postgres?connect_timeout=10&options=reference%3D${LEGACY_VALID_REF}`, - ), - ).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(dbConfig.resolves).toHaveLength(1); + expect(dbConfig.resolves[0]?.connType).toBe("linked"); + // --linked is the workdir's own project: keep workdir-scoped credentials. + expect(dbConfig.resolves[0]?.adHocProjectRef).toBe(false); + }); + }); - it.live("retries remote pg-meta through the IPv4 pooler on a container IPv6 failure", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const child = mockSequentialChildProcessSpawner([ - { - exitCode: 1, - stderr: [ - 'could not translate host name "db.abcdefghijklmnopqrst.supabase.co" to address: No address associated with hostname', - ], - }, - { exitCode: 0, stdout: ["type RetriedViaPooler struct {}"] }, - ]); - const poolerConn: LegacyPgConnInput = { + it.live("preserves resolver URL options for remote non-TypeScript typegen", () => { + const { layer, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorOutput: "type PublicMovies struct {}", + dbConfigResolve: () => + Effect.succeed( + remoteResolvedConfig({ host: "127.0.0.1", - port, + port: 5432, user: `postgres.${LEGACY_VALID_REF}`, password: "pooler-password", database: "postgres", - }; - const { layer, out, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: `db.${LEGACY_VALID_REF}.supabase.co`, - port, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - poolerFallback: Option.some(poolerConn), - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer)), - ); + options: `reference=${LEGACY_VALID_REF}`, + }), + ), + }); - expect(out.stdoutText).toContain("type RetriedViaPooler struct {}"); - expect(out.stderrText).toContain("does not support IPv6"); - expect(out.stderrText).toContain("Retrying via the IPv4 connection pooler."); - expect(child.spawned).toHaveLength(2); - expect( - dockerEnv(child.spawned[0]?.args ?? []).has( - `PG_META_DB_URL=postgresql://postgres:direct-password@db.${LEGACY_VALID_REF}.supabase.co:${port}/postgres?connect_timeout=10`, - ), - ).toBe(true); - expect( - dockerEnv(child.spawned[1]?.args ?? []).has( - `PG_META_DB_URL=postgresql://postgres.${LEGACY_VALID_REF}:pooler-password@127.0.0.1:${port}/postgres?connect_timeout=10`, - ), - ).toBe(true); - expect(dbConfig.poolerFallbacks).toHaveLength(1); - expect(dbConfig.poolerFallbacks[0]?.connType).toBe("linked"); - expect(dbConfig.poolerFallbacks[0]?.adHocProjectRef).toBe(true); + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + ).pipe(Effect.provide(layer)); - it.live("retries remote pg-meta through the IPv4 pooler on Node ENETUNREACH stderr", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const child = mockSequentialChildProcessSpawner([ - { - exitCode: 1, - stderr: ["connect ENETUNREACH 2600:1f18::1:5432 - Local (:::0)"], - }, - { exitCode: 0, stdout: ["type RetriedViaPooler struct {}"] }, - ]); - const poolerConn: LegacyPgConnInput = { - host: "127.0.0.1", - port, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", + // Supavisor pooler URLs carry the tenant in `options=reference=`; + // the resolved connection must reach the driver intact. + expect(generator.calls[0]?.conn.options).toBe(`reference=${LEGACY_VALID_REF}`); + expect(generator.calls[0]?.conn.user).toBe(`postgres.${LEGACY_VALID_REF}`); + }); + }); + + it.live("retries remote generation through the IPv4 pooler on an IPv6 connect failure", () => { + const poolerConn: LegacyPgConnInput = { + host: "127.0.0.1", + port: 6543, + user: `postgres.${LEGACY_VALID_REF}`, + password: "pooler-password", + database: "postgres", + }; + const { layer, out, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorResults: [ + Effect.fail(IPV6_CONNECT_FAILURE), + Effect.succeed("type RetriedViaPooler struct {}"), + ], + dbConfigResolve: () => + Effect.succeed( + remoteResolvedConfig({ + host: `db.${LEGACY_VALID_REF}.supabase.co`, + port: 5432, + user: "postgres", + password: "direct-password", database: "postgres", - }; - const { layer, out, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: `db.${LEGACY_VALID_REF}.supabase.co`, - port, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - poolerFallback: Option.some(poolerConn), - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer)), - ); + }), + ), + poolerFallback: Option.some(poolerConn), + }); - expect(out.stdoutText).toContain("type RetriedViaPooler struct {}"); - expect(child.spawned).toHaveLength(2); - expect(dbConfig.poolerFallbacks).toHaveLength(1); + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + ).pipe(Effect.provide(layer)); - it.live("does not retry remote pg-meta when the container failure is not IPv6", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const child = mockSequentialChildProcessSpawner([ - { exitCode: 1, stderr: ["permission denied for schema public"] }, - ]); - const { layer, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: `db.${LEGACY_VALID_REF}.supabase.co`, - port, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - poolerFallback: Option.some({ - host: "127.0.0.1", - port, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", - database: "postgres", - }), - }); + expect(out.stdoutText).toContain("type RetriedViaPooler struct {}"); + expect(out.stderrText).toContain("does not support IPv6"); + expect(out.stderrText).toContain("Retrying via the IPv4 connection pooler."); + expect(generator.calls).toHaveLength(2); + expect(generator.calls[0]?.conn.host).toBe(`db.${LEGACY_VALID_REF}.supabase.co`); + expect(generator.calls[1]?.conn).toEqual(poolerConn); + expect(generator.calls[1]?.isLocal).toBe(false); + expect(dbConfig.poolerFallbacks).toHaveLength(1); + expect(dbConfig.poolerFallbacks[0]?.connType).toBe("linked"); + expect(dbConfig.poolerFallbacks[0]?.adHocProjectRef).toBe(true); + }); + }); - const exit = await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer), Effect.exit), - ); + it.live("retries remote generation through the IPv4 pooler on a Node ENETUNREACH failure", () => { + const poolerConn: LegacyPgConnInput = { + host: "127.0.0.1", + port: 6543, + user: `postgres.${LEGACY_VALID_REF}`, + password: "pooler-password", + database: "postgres", + }; + const { layer, out, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorResults: [ + Effect.fail( + new LegacyDbConnectError({ + message: + "failed to connect to postgres: connect ENETUNREACH 2600:1f18::1:5432 - Local (:::0)", + }), + ), + Effect.succeed("type RetriedViaPooler struct {}"), + ], + dbConfigResolve: () => + Effect.succeed( + remoteResolvedConfig({ + host: `db.${LEGACY_VALID_REF}.supabase.co`, + port: 5432, + user: "postgres", + password: "direct-password", + database: "postgres", + }), + ), + poolerFallback: Option.some(poolerConn), + }); - expect(Exit.isFailure(exit)).toBe(true); - expect(child.spawned).toHaveLength(1); - expect(dbConfig.poolerFallbacks).toHaveLength(0); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", + }), + ).pipe(Effect.provide(layer)); - it.live( - "does not run pooler fallback a second time when the retry also exits with IPv6 stderr", - () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const child = mockSequentialChildProcessSpawner([ - { - exitCode: 1, - stderr: [ - `could not translate host name "db.${LEGACY_VALID_REF}.supabase.co" to address: No address associated with hostname`, - ], - }, - { - exitCode: 1, - stderr: [ - `could not translate host name "db.${LEGACY_VALID_REF}.supabase.co" to address: No address associated with hostname`, - ], - }, - ]); - const { layer, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: `db.${LEGACY_VALID_REF}.supabase.co`, - port, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - poolerFallback: Option.some({ - host: "127.0.0.1", - port, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", - database: "postgres", - }), - }); - - const exit = await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer), Effect.exit), - ); + expect(out.stdoutText).toContain("type RetriedViaPooler struct {}"); + expect(generator.calls).toHaveLength(2); + expect(dbConfig.poolerFallbacks).toHaveLength(1); + }); + }); - expect(Exit.isFailure(exit)).toBe(true); - expect(child.spawned).toHaveLength(2); - expect(dbConfig.poolerFallbacks).toHaveLength(1); + it.live("does not retry remote generation when the failure is not IPv6", () => { + const { layer, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorResults: [ + Effect.fail( + new LegacyGenTypesMetadataError({ + message: "failed to introspect database: permission denied for schema public", + }), + ), + ], + dbConfigResolve: () => + Effect.succeed( + remoteResolvedConfig({ + host: `db.${LEGACY_VALID_REF}.supabase.co`, + port: 5432, + user: "postgres", + password: "direct-password", + database: "postgres", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + ), + poolerFallback: Option.some({ + host: "127.0.0.1", + port: 6543, + user: `postgres.${LEGACY_VALID_REF}`, + password: "pooler-password", + database: "postgres", }), - ); + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + expect(generator.calls).toHaveLength(1); + expect(dbConfig.poolerFallbacks).toHaveLength(0); + }); + }); + + it.live("does not run pooler fallback a second time when the retry also fails with IPv6", () => { + const { layer, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorResults: [Effect.fail(IPV6_CONNECT_FAILURE), Effect.fail(IPV6_CONNECT_FAILURE)], + dbConfigResolve: () => + Effect.succeed( + remoteResolvedConfig({ + host: `db.${LEGACY_VALID_REF}.supabase.co`, + port: 5432, + user: "postgres", + password: "direct-password", + database: "postgres", + }), + ), + poolerFallback: Option.some({ + host: "127.0.0.1", + port: 6543, + user: `postgres.${LEGACY_VALID_REF}`, + password: "pooler-password", + database: "postgres", + }), + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + expect(generator.calls).toHaveLength(2); + expect(dbConfig.poolerFallbacks).toHaveLength(1); + }); + }); it.live( - "does not retry remote pg-meta when the resolved connection is already a pooler host", - () => - Effect.tryPromise({ - try: () => - Effect.runPromise( - Effect.gen(function* () { - const child = mockSequentialChildProcessSpawner([ - { - exitCode: 1, - stderr: [ - `could not translate host name "db.${LEGACY_VALID_REF}.supabase.co" to address: No address associated with hostname`, - ], - }, - ]); - const { layer, out, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: "aws-0-us-east-1.pooler.supabase.com", - port: 5432, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", - database: "postgres", - }), - ), - poolerFallback: Option.some({ - host: "aws-0-us-east-1.pooler.supabase.com", - port: 5432, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", - database: "postgres", - }), - }); - - const exit = yield* legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer), Effect.exit); - - expect(Exit.isFailure(exit)).toBe(true); - expect(child.spawned).toHaveLength(1); - expect(dbConfig.poolerFallbacks).toHaveLength(0); - expect(out.stderrText).not.toContain("Retrying via the IPv4 connection pooler."); + "does not retry remote generation when the resolved connection is already a pooler host", + () => { + const { layer, out, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorResults: [Effect.fail(IPV6_CONNECT_FAILURE)], + dbConfigResolve: () => + Effect.succeed( + remoteResolvedConfig({ + host: "aws-0-us-east-1.pooler.supabase.com", + port: 5432, + user: `postgres.${LEGACY_VALID_REF}`, + password: "pooler-password", + database: "postgres", }), ), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + poolerFallback: Option.some({ + host: "aws-0-us-east-1.pooler.supabase.com", + port: 5432, + user: `postgres.${LEGACY_VALID_REF}`, + password: "pooler-password", + database: "postgres", + }), + }); - it.live("retries remote pg-meta when the TLS probe fails with ENETUNREACH", () => - Effect.tryPromise({ - try: () => - Effect.runPromise( - Effect.gen(function* () { - let probeCalls = 0; - const child = mockSequentialChildProcessSpawner([ - { exitCode: 0, stdout: ["type RetriedAfterProbeFailure struct {}"] }, - ]); - const { layer, out, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => - Effect.gen(function* () { - probeCalls += 1; - if (probeCalls === 1) { - return yield* Effect.fail( - new LegacyPgDeltaSslProbeError({ - message: "network is unreachable", - cause: Object.assign(new Error(), { code: "ENETUNREACH" }), - }), - ); - } - return false; - }), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: `db.${LEGACY_VALID_REF}.supabase.co`, - port: 5432, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - poolerFallback: Option.some({ - host: "aws-0-us-east-1.pooler.supabase.com", - port: 5432, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", - database: "postgres", - }), - }); + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", + }), + ).pipe(Effect.provide(layer), Effect.exit); - yield* legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer)); + expect(Exit.isFailure(exit)).toBe(true); + expect(generator.calls).toHaveLength(1); + expect(dbConfig.poolerFallbacks).toHaveLength(0); + expect(out.stderrText).not.toContain("Retrying via the IPv4 connection pooler."); + }); + }, + ); - expect(out.stdoutText).toContain("type RetriedAfterProbeFailure struct {}"); - expect(probeCalls).toBe(2); - expect(child.spawned).toHaveLength(1); - expect(dbConfig.poolerFallbacks).toHaveLength(1); + it.live("preserves the original generation error when pooler fallback resolution fails", () => { + const { layer, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorResults: [Effect.fail(IPV6_CONNECT_FAILURE)], + dbConfigResolve: () => + Effect.succeed( + remoteResolvedConfig({ + host: `db.${LEGACY_VALID_REF}.supabase.co`, + port: 5432, + user: "postgres", + password: "direct-password", + database: "postgres", }), ), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + poolerFallbackFails: true, + }); - it.live("does not retry remote pg-meta when the TLS probe fails with ECONNREFUSED", () => - Effect.tryPromise({ - try: () => - Effect.runPromise( - Effect.gen(function* () { - const child = mockSequentialChildProcessSpawner([ - { exitCode: 0, stdout: ["should not spawn"] }, - ]); - const { layer, dbConfig } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => - Effect.fail( - new LegacyPgDeltaSslProbeError({ - message: "connection refused", - cause: Object.assign(new Error(), { code: "ECONNREFUSED" }), - }), - ), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: `db.${LEGACY_VALID_REF}.supabase.co`, - port: 5432, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - poolerFallback: Option.some({ - host: "aws-0-us-east-1.pooler.supabase.com", - port: 5432, - user: `postgres.${LEGACY_VALID_REF}`, - password: "pooler-password", - database: "postgres", - }), - }); + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", + }), + ).pipe(Effect.provide(layer), Effect.exit); - const exit = yield* legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(String(exit.cause)).toContain("could not translate host name"); + expect(String(exit.cause)).not.toContain("pooler fallback failed"); + } + expect(generator.calls).toHaveLength(1); + }); + }); + + it.live("uses remote config schemas for explicit project-ref typegen", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-remote-config-")); + writeConfig( + workdir, + [ + 'project_id = "base"', + "", + "[api]", + 'schemas = ["public"]', + "", + "[remotes.staging]", + `project_id = "${LEGACY_VALID_REF}"`, + "", + "[remotes.staging.api]", + 'schemas = ["private"]', + "", + ].join("\n"), + ); + const { layer, generator } = setup({ + workdir, + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + generatorOutput: "type PrivateMovies struct {}", + }); - expect(Exit.isFailure(exit)).toBe(true); - expect(child.spawned).toHaveLength(0); - expect(dbConfig.poolerFallbacks).toHaveLength(0); + return Effect.gen(function* () { + try { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", }), - ), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + ).pipe(Effect.provide(layer)); + } finally { + rmSync(workdir, { recursive: true, force: true }); + } - it.live("preserves the original remote pg-meta error when pooler fallback resolution fails", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const child = mockSequentialChildProcessSpawner([ - { - exitCode: 1, - stderr: [ - 'could not translate host name "db.abcdefghijklmnopqrst.supabase.co" to address: No address associated with hostname', - ], - }, - ]); - const { layer } = setup({ - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: `db.${LEGACY_VALID_REF}.supabase.co`, - port, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - poolerFallbackFails: true, - }); - - const exit = await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer), Effect.exit), - ); + expect(generator.calls[0]?.includedSchemas).toEqual(["public", "private"]); + }); + }); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(String(exit.cause)).toContain("error running container: exit 1"); - expect(String(exit.cause)).not.toContain("pooler fallback failed"); - } - expect(child.spawned).toHaveLength(1); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + it.live("uses remote config schemas for linked typegen", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-linked-config-")); + writeConfig( + workdir, + [ + 'project_id = "base"', + "", + "[api]", + 'schemas = ["public"]', + "", + "[remotes.staging]", + `project_id = "${LEGACY_VALID_REF}"`, + "", + "[remotes.staging.api]", + 'schemas = ["private"]', + "", + ].join("\n"), + ); + const { layer, generator } = setup({ + workdir, + projectId: Option.some(LEGACY_VALID_REF), + args: ["gen", "types", "--lang", "go", "--linked"], + generatorOutput: "type PrivateMovies struct {}", + }); - it.live("uses remote config schemas for explicit project-ref pg-meta typegen", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-remote-config-")); - writeConfig( - workdir, - [ - 'project_id = "base"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[remotes.staging]", - `project_id = "${LEGACY_VALID_REF}"`, - "", - "[remotes.staging.api]", - 'schemas = ["private"]', - "", - ].join("\n"), - ); - const docker = captureDockerRun(); - const { layer } = setup({ - workdir, - args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], - childStdout: ["type PrivateMovies struct {}"], - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: "127.0.0.1", - port, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - onSpawn: docker.onSpawn, - }); - - try { - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "go", - }), - ).pipe(Effect.provide(layer)), - ); - } finally { - rmSync(workdir, { recursive: true, force: true }); - } + return Effect.gen(function* () { + try { + yield* legacyGenTypes( + defaultFlags({ + linked: true, + lang: "go", + }), + ).pipe(Effect.provide(layer)); + } finally { + rmSync(workdir, { recursive: true, force: true }); + } - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,private")).toBe( - true, - ); - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public")).toBe(false); + expect(generator.calls[0]?.includedSchemas).toEqual(["public", "private"]); + }); + }); + + it.live("falls back to preview branch config for non-TypeScript project refs", () => { + const { layer, api, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], + generatorOutput: "class PublicMovies(BaseModel):", + getProject: () => Effect.fail(statusApiError(404, `{"message":"Preview branch not found"}`)), + getABranchConfig: ({ branch_id_or_ref }) => + Effect.succeed({ + ref: branch_id_or_ref, + postgres_version: "15.1", + postgres_engine: "15", + release_channel: "ga", + status: "ACTIVE_HEALTHY", + db_host: "127.0.0.1", + db_port: 5432, + db_user: "branch_user", + db_pass: "branch-password", + jwt_secret: "secret", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + createLoginRole: ({ ref }) => + Effect.fail(new Error(`unexpected login role creation for ${ref}`)), + }); - it.live("uses remote config schemas for linked pg-meta typegen", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-linked-config-")); - writeConfig( - workdir, - [ - 'project_id = "base"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[remotes.staging]", - `project_id = "${LEGACY_VALID_REF}"`, - "", - "[remotes.staging.api]", - 'schemas = ["private"]', - "", - ].join("\n"), - ); - const docker = captureDockerRun(); - const { layer } = setup({ - workdir, - projectId: Option.some(LEGACY_VALID_REF), - args: ["gen", "types", "--lang", "go", "--linked"], - childStdout: ["type PrivateMovies struct {}"], - dbConfigResolve: () => - Effect.succeed( - remoteResolvedConfig({ - host: "127.0.0.1", - port, - user: "postgres", - password: "direct-password", - database: "postgres", - }), - ), - onSpawn: docker.onSpawn, - }); - - try { - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - linked: true, - lang: "go", - }), - ).pipe(Effect.provide(layer)), - ); - } finally { - rmSync(workdir, { recursive: true, force: true }); - } + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "python", + }), + ).pipe(Effect.provide(layer)); - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,private")).toBe( - true, - ); - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public")).toBe(false); + expect(api.requests).toContainEqual({ + method: "getProject", + input: { ref: LEGACY_VALID_REF }, + }); + expect(api.requests).toContainEqual({ + method: "getABranchConfig", + input: { branch_id_or_ref: LEGACY_VALID_REF }, + }); + expect(api.requests).not.toContainEqual( + expect.objectContaining({ method: "createLoginRole" }), + ); + expect(dbConfig.resolves).toHaveLength(0); + expect(generator.calls[0]?.conn).toEqual({ + host: "127.0.0.1", + port: 5432, + user: "branch_user", + password: "branch-password", + database: "postgres", + }); + expect(generator.calls[0]?.isLocal).toBe(false); + }); + }); + + it.live("retries preview branch generation through the branch IPv4 pooler", () => { + const poolerHost = "aws-0-us-east-1.pooler.supabase.com"; + const { layer, api, generator } = setup({ + args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], + generatorResults: [ + Effect.fail(IPV6_CONNECT_FAILURE), + Effect.succeed("class RetriedViaBranchPooler(BaseModel):"), + ], + getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), + getABranchConfig: ({ branch_id_or_ref }) => + Effect.succeed({ + ref: branch_id_or_ref, + postgres_version: "15.1", + postgres_engine: "15", + release_channel: "ga", + status: "ACTIVE_HEALTHY", + db_host: `db.${branch_id_or_ref}.supabase.co`, + db_port: 5432, + db_user: "branch_user", + db_pass: "branch-password", + jwt_secret: "secret", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + getPoolerConfig: ({ ref }) => + Effect.succeed([ + { + identifier: "primary", + database_type: "PRIMARY", + is_using_scram_auth: true, + db_user: "postgres", + db_host: "db.example", + db_port: 5432, + db_name: "postgres", + connection_string: `postgres://postgres.${ref}:[YOUR-PASSWORD]@${poolerHost}:6543/postgres`, + connectionString: `postgres://postgres.${ref}:[YOUR-PASSWORD]@${poolerHost}:6543/postgres`, + default_pool_size: null, + max_client_conn: null, + pool_mode: "transaction", + }, + ]), + }); - it.live("falls back to preview branch config for non-TypeScript project refs", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer, api, dbConfig } = setup({ - args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], - childStdout: ["class PublicMovies(BaseModel):"], - getProject: () => - Effect.fail(statusApiError(404, `{"message":"Preview branch not found"}`)), - getABranchConfig: ({ branch_id_or_ref }) => - Effect.succeed({ - ref: branch_id_or_ref, - postgres_version: "15.1", - postgres_engine: "15", - release_channel: "ga", - status: "ACTIVE_HEALTHY", - db_host: "127.0.0.1", - db_port: port, - db_user: "branch_user", - db_pass: "branch-password", - jwt_secret: "secret", - }), - createLoginRole: ({ ref }) => - Effect.fail(new Error(`unexpected login role creation for ${ref}`)), - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "python", - }), - ).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "python", + }), + ).pipe(Effect.provide(layer)); - expect(api.requests).toContainEqual({ - method: "getProject", - input: { ref: LEGACY_VALID_REF }, - }); - expect(api.requests).toContainEqual({ - method: "getABranchConfig", - input: { branch_id_or_ref: LEGACY_VALID_REF }, - }); - expect(api.requests).not.toContainEqual( - expect.objectContaining({ method: "createLoginRole" }), - ); - expect(dbConfig.resolves).toHaveLength(0); - expect( - docker.env.has( - `PG_META_DB_URL=postgresql://branch_user:branch-password@127.0.0.1:${port}/postgres?connect_timeout=10`, - ), - ).toBe(true); + expect(api.requests).toContainEqual({ + method: "getPoolerConfig", + input: { ref: LEGACY_VALID_REF }, + }); + expect(generator.calls).toHaveLength(2); + expect(generator.calls[1]?.conn.host).toBe(poolerHost); + expect(generator.calls[1]?.conn.user).toBe(`postgres.${LEGACY_VALID_REF}`); + // The branch credentials replace the pooler URL's placeholder password. + expect(generator.calls[1]?.conn.password).toBe("branch-password"); + }); + }); + + it.live("skips preview branch pooler fallback when the pooler URL fails validation", () => { + const { layer, api, generator } = setup({ + args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], + generatorResults: [Effect.fail(IPV6_CONNECT_FAILURE)], + getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), + getABranchConfig: ({ branch_id_or_ref }) => + Effect.succeed({ + ref: branch_id_or_ref, + postgres_version: "15.1", + postgres_engine: "15", + release_channel: "ga", + status: "ACTIVE_HEALTHY", + db_host: `db.${branch_id_or_ref}.supabase.co`, + db_port: 5432, + db_user: "branch_user", + db_pass: "branch-password", + jwt_secret: "secret", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + getPoolerConfig: ({ ref }) => + Effect.succeed([ + { + identifier: "primary", + database_type: "PRIMARY", + is_using_scram_auth: true, + db_user: "postgres", + db_host: "db.example", + db_port: 5432, + db_name: "postgres", + connection_string: `postgres://postgres.${ref}:[YOUR-PASSWORD]@pooler.example.com:6543/postgres`, + connectionString: `postgres://postgres.${ref}:[YOUR-PASSWORD]@pooler.example.com:6543/postgres`, + default_pool_size: null, + max_client_conn: null, + pool_mode: "transaction", + }, + ]), + }); - it.live("retries preview branch pg-meta through the branch IPv4 pooler", () => - Effect.tryPromise({ - try: () => - Effect.runPromise( - Effect.gen(function* () { - const poolerHost = "aws-0-us-east-1.pooler.supabase.com"; - const child = mockSequentialChildProcessSpawner([ - { - exitCode: 1, - stderr: [ - `could not translate host name "db.${LEGACY_VALID_REF}.supabase.co" to address: No address associated with hostname`, - ], - }, - { exitCode: 0, stdout: ["class RetriedViaBranchPooler(BaseModel):"] }, - ]); - const { layer, api } = setup({ - args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), - getABranchConfig: ({ branch_id_or_ref }) => - Effect.succeed({ - ref: branch_id_or_ref, - postgres_version: "15.1", - postgres_engine: "15", - release_channel: "ga", - status: "ACTIVE_HEALTHY", - db_host: `db.${branch_id_or_ref}.supabase.co`, - db_port: 5432, - db_user: "branch_user", - db_pass: "branch-password", - jwt_secret: "secret", - }), - getPoolerConfig: ({ ref }) => - Effect.succeed([ - { - identifier: "primary", - database_type: "PRIMARY", - is_using_scram_auth: true, - db_user: "postgres", - db_host: "db.example", - db_port: 5432, - db_name: "postgres", - connection_string: `postgres://postgres.${ref}:[YOUR-PASSWORD]@${poolerHost}:6543/postgres`, - connectionString: `postgres://postgres.${ref}:[YOUR-PASSWORD]@${poolerHost}:6543/postgres`, - default_pool_size: null, - max_client_conn: null, - pool_mode: "transaction", - }, - ]), - }); - - yield* legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "python", - }), - ).pipe(Effect.provide(layer)); - - expect(api.requests).toContainEqual({ - method: "getPoolerConfig", - input: { ref: LEGACY_VALID_REF }, - }); - expect(child.spawned).toHaveLength(2); - expect( - dockerEnv(child.spawned[1]?.args ?? []).has( - `PG_META_DB_URL=postgresql://postgres.${LEGACY_VALID_REF}:branch-password@${poolerHost}:5432/postgres?connect_timeout=10`, - ), - ).toBe(true); - }), - ), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "python", + }), + ).pipe(Effect.provide(layer), Effect.exit); - it.live("skips preview branch pooler fallback when the pooler URL fails validation", () => - Effect.tryPromise({ - try: () => - Effect.runPromise( - Effect.gen(function* () { - const child = mockSequentialChildProcessSpawner([ - { - exitCode: 1, - stderr: [ - `could not translate host name "db.${LEGACY_VALID_REF}.supabase.co" to address: No address associated with hostname`, - ], - }, - ]); - const { layer, api } = setup({ - args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], - childLayer: child.layer, - sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { - requireSsl: () => Effect.succeed(false), - requireSslForHost: () => Effect.succeed(false), - }), - getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), - getABranchConfig: ({ branch_id_or_ref }) => - Effect.succeed({ - ref: branch_id_or_ref, - postgres_version: "15.1", - postgres_engine: "15", - release_channel: "ga", - status: "ACTIVE_HEALTHY", - db_host: `db.${branch_id_or_ref}.supabase.co`, - db_port: 5432, - db_user: "branch_user", - db_pass: "branch-password", - jwt_secret: "secret", - }), - getPoolerConfig: ({ ref }) => - Effect.succeed([ - { - identifier: "primary", - database_type: "PRIMARY", - is_using_scram_auth: true, - db_user: "postgres", - db_host: "db.example", - db_port: 5432, - db_name: "postgres", - connection_string: `postgres://postgres.${ref}:[YOUR-PASSWORD]@pooler.example.com:6543/postgres`, - connectionString: `postgres://postgres.${ref}:[YOUR-PASSWORD]@pooler.example.com:6543/postgres`, - default_pool_size: null, - max_client_conn: null, - pool_mode: "transaction", - }, - ]), - }); - - const exit = yield* legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "python", - }), - ).pipe(Effect.provide(layer), Effect.exit); - - expect(Exit.isFailure(exit)).toBe(true); - expect(api.requests).toContainEqual({ - method: "getPoolerConfig", - input: { ref: LEGACY_VALID_REF }, - }); - expect(child.spawned).toHaveLength(1); - }), - ), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(Exit.isFailure(exit)).toBe(true); + expect(api.requests).toContainEqual({ + method: "getPoolerConfig", + input: { ref: LEGACY_VALID_REF }, + }); + expect(generator.calls).toHaveLength(1); + }); + }); - it.live("falls back to preview branch config for any project 404 body", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer, api, dbConfig } = setup({ - args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], - childStdout: ["class PublicMovies(BaseModel):"], - // The Management API's 404 wording is not guaranteed; a generic body - // must still route to the branch config endpoint. - getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), - getABranchConfig: ({ branch_id_or_ref }) => - Effect.succeed({ - ref: branch_id_or_ref, - postgres_version: "15.1", - postgres_engine: "15", - release_channel: "ga", - status: "ACTIVE_HEALTHY", - db_host: "127.0.0.1", - db_port: port, - db_user: "branch_user", - db_pass: "branch-password", - jwt_secret: "secret", - }), - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - projectId: Option.some(LEGACY_VALID_REF), - lang: "python", - }), - ).pipe(Effect.provide(layer)), - ); + it.live("falls back to preview branch config for any project 404 body", () => { + const { layer, api, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], + generatorOutput: "class PublicMovies(BaseModel):", + // The Management API's 404 wording is not guaranteed; a generic body + // must still route to the branch config endpoint. + getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), + getABranchConfig: ({ branch_id_or_ref }) => + Effect.succeed({ + ref: branch_id_or_ref, + postgres_version: "15.1", + postgres_engine: "15", + release_channel: "ga", + status: "ACTIVE_HEALTHY", + db_host: "127.0.0.1", + db_port: 5432, + db_user: "branch_user", + db_pass: "branch-password", + jwt_secret: "secret", + }), + }); - expect(api.requests).toContainEqual({ - method: "getABranchConfig", - input: { branch_id_or_ref: LEGACY_VALID_REF }, - }); - expect(dbConfig.resolves).toHaveLength(0); - expect( - docker.env.has( - `PG_META_DB_URL=postgresql://branch_user:branch-password@127.0.0.1:${port}/postgres?connect_timeout=10`, - ), - ).toBe(true); + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "python", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + ).pipe(Effect.provide(layer)); + + expect(api.requests).toContainEqual({ + method: "getABranchConfig", + input: { branch_id_or_ref: LEGACY_VALID_REF }, + }); + expect(dbConfig.resolves).toHaveLength(0); + expect(generator.calls[0]?.conn.user).toBe("branch_user"); + expect(generator.calls[0]?.conn.password).toBe("branch-password"); + }); + }); it.live("fails clearly when preview branch config does not include DB credentials", () => { const { layer } = setup({ @@ -2293,13 +1665,159 @@ describe("legacy gen types", () => { const exit = yield* legacyGenTypes( defaultFlags({ projectId: Option.some(LEGACY_VALID_REF), - lang: "python", + lang: "python", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(String(exit.cause)).toContain("Preview branch database credentials are unavailable"); + } + }); + }); + + it.live("surfaces a non-404 project lookup failure for non-TypeScript generation", () => { + const { layer, dbConfig, generator } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + getProject: () => Effect.fail(statusApiError(500, `{"message":"boom"}`)), + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + // Only a 404 routes to the preview-branch fallback; any other status + // surfaces as the mapped project database config error. + expect(String(exit.cause)).toContain("unexpected project database config status 500"); + } + expect(dbConfig.resolves).toHaveLength(0); + expect(generator.calls).toHaveLength(0); + }); + }); + + it.live("maps project lookup network failures for non-TypeScript generation", () => { + const { layer } = setup({ + args: ["gen", "types", "--lang", "go", "--project-id", LEGACY_VALID_REF], + getProject: () => Effect.fail(new Error("network error")), + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "go", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(String(exit.cause)).toContain("failed to get project database config"); + } + }); + }); + + it.live("maps preview branch config network failures after the project 404", () => { + const { layer } = setup({ + args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], + getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), + getABranchConfig: () => Effect.fail(new Error("network error")), + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "python", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(String(exit.cause)).toContain("failed to get preview branch database config"); + } + }); + }); + + it.live("maps preview branch config status failures after the project 404", () => { + const { layer, generator } = setup({ + args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], + getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), + getABranchConfig: () => Effect.fail(statusApiError(500, `{"message":"boom"}`)), + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "python", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(String(exit.cause)).toContain( + "unexpected preview branch database config status 500", + ); + } + expect(generator.calls).toHaveLength(0); + }); + }); + + it.live("skips preview branch pooler fallback when no primary pooler is configured", () => { + const { layer, generator } = setup({ + args: ["gen", "types", "--lang", "python", "--project-id", LEGACY_VALID_REF], + generatorResults: [Effect.fail(IPV6_CONNECT_FAILURE)], + getProject: () => Effect.fail(statusApiError(404, `{"message":"Not found"}`)), + getABranchConfig: ({ branch_id_or_ref }) => + Effect.succeed({ + ref: branch_id_or_ref, + postgres_version: "15.1", + postgres_engine: "15", + release_channel: "ga", + status: "ACTIVE_HEALTHY", + db_host: `db.${branch_id_or_ref}.supabase.co`, + db_port: 5432, + db_user: "branch_user", + db_pass: "branch-password", + jwt_secret: "secret", + }), + getPoolerConfig: () => Effect.succeed([]), + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), + lang: "python", + }), + ).pipe(Effect.provide(layer), Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + expect(generator.calls).toHaveLength(1); + }); + }); + + it.live("maps project type generation status failures", () => { + const { layer } = setup({ + generateTypescriptTypes: () => Effect.fail(statusApiError(500, "generation broke")), + }); + + return Effect.gen(function* () { + const exit = yield* legacyGenTypes( + defaultFlags({ + projectId: Option.some(LEGACY_VALID_REF), }), ).pipe(Effect.provide(layer), Effect.exit); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { - expect(String(exit.cause)).toContain("Preview branch database credentials are unavailable"); + expect(String(exit.cause)).toContain("failed to retrieve generated types"); } }); }); @@ -2325,332 +1843,209 @@ describe("legacy gen types", () => { }); }); - it.live("spawns pg-meta for local generation and forwards child output", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - "port = 54321", - 'schemas = ["public", "custom"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); - - const { layer, out, child, linkedProjectCache } = setup({ - workdir, - childStdout: ["export type Database = {};"], - childStderr: ["pg-meta warning"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)), - ); + it.live("generates locally through the native generator", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-")); + writeConfig( + workdir, + [ + 'project_id = "demo"', + "", + "[api]", + "port = 54321", + 'schemas = ["public", "custom"]', + "", + "[db]", + "port = 54322", + ].join("\n"), + ); - expect(out.stderrText).toContain("Connecting to db 5432"); - expect(out.stderrText).toContain("pg-meta warning"); - expect(out.stdoutText).toContain("export type Database = {};"); - expect(child.spawned).toHaveLength(2); - expect(child.spawned[0]).toEqual({ - command: "docker", - args: ["container", "inspect", "supabase_db_demo"], - }); - expect(child.spawned[1]?.command).toBe("docker"); - expect(child.spawned[1]?.args).toContain("--network"); - expect(child.spawned[1]?.args).toContain("supabase_network_demo"); - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,custom")).toBe( - true, - ); - expect(child.spawned[1]?.args).toContain(resolvePgmetaImage()); - expect(child.spawned[1]?.args.slice(-2)).toEqual(["node", "dist/server/server.js"]); - // The local/db-url paths have no project ref, so they must not - // populate the linked-project cache. - expect(linkedProjectCache.cached).toBe(false); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + const { layer, out, child, linkedProjectCache, generator, dbConfig } = setup({ + workdir, + generatorOutput: "export type Database = {};", + }); - it.live("falls back to podman when the docker executable is missing for local generation", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-podman-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); - const child = mockDockerMissingChildProcessSpawner([ - { exitCode: 0 }, - { exitCode: 0, stdout: ["export type Database = {};"] }, - ]); - const { layer, out } = setup({ - workdir, - childLayer: child.layer, - }); - - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); - expect(out.stdoutText).toContain("export type Database = {};"); - expect(child.spawned[0]).toEqual({ - command: "docker", - args: ["container", "inspect", "supabase_db_demo"], - }); - expect(child.spawned[1]).toEqual({ - command: "podman", - args: ["container", "inspect", "supabase_db_demo"], - }); - expect(child.spawned[2]?.command).toBe("docker"); - expect(child.spawned[2]?.args).toContain("run"); - expect(child.spawned[3]?.command).toBe("podman"); - expect(child.spawned[3]?.args).toContain("run"); - expect(child.spawned[3]?.args).toContain("supabase_network_demo"); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(out.stderrText).toContain("Connecting to 127.0.0.1 54322"); + expect(out.stdoutText).toBe("export type Database = {};\n"); + // The only remaining subprocess is the local-stack `container inspect`; + // generation itself is in-process. + expect(child.spawned).toEqual([ + { command: "docker", args: ["container", "inspect", "supabase_db_demo"] }, + ]); + expect(generator.calls).toHaveLength(1); + expect(generator.calls[0]?.conn).toEqual({ + host: "127.0.0.1", + port: 54322, + user: "postgres", + password: "postgres", + database: "postgres", + }); + expect(generator.calls[0]?.isLocal).toBe(true); + expect(generator.calls[0]?.includedSchemas).toEqual(["public", "custom"]); + // The local path never consults the DB config resolver. + expect(dbConfig.resolves).toHaveLength(0); + // The local/db-url paths have no project ref, so they must not + // populate the linked-project cache. + expect(linkedProjectCache.cached).toBe(false); + }); + }); - it.live("uses sanitized local docker ids and env-backed local db passwords", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-sanitized-")); - writeConfig( - workdir, - [ - 'project_id = "..demo project with spaces"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); + it.live("falls back to podman when the docker executable is missing for local generation", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-podman-")); + writeConfig( + workdir, + ['project_id = "demo"', "", "[api]", 'schemas = ["public"]', "", "[db]", "port = 54322"].join( + "\n", + ), + ); + const child = mockDockerMissingChildProcessSpawner([{ exitCode: 0 }]); + const { layer, out } = setup({ + workdir, + childLayer: child.layer, + generatorOutput: "export type Database = {};", + }); - const previousPassword = process.env["SUPABASE_DB_PASSWORD"]; - process.env["SUPABASE_DB_PASSWORD"] = "secret-password"; - try { - const { layer, child } = setup({ - workdir, - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); - expect(child.spawned[0]).toEqual({ - command: "docker", - args: ["container", "inspect", "supabase_db_demo_project_with_spaces"], - }); - expect(child.spawned[1]?.args).toContain("supabase_network_demo_project_with_spaces"); - expect( - docker.env.has( - "PG_META_DB_URL=postgresql://postgres:secret-password@db:5432/postgres?connect_timeout=10", - ), - ).toBe(true); - } finally { - if (previousPassword === undefined) { - delete process.env["SUPABASE_DB_PASSWORD"]; - } else { - process.env["SUPABASE_DB_PASSWORD"] = previousPassword; - } - } - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(out.stdoutText).toContain("export type Database = {};"); + expect(child.spawned).toEqual([ + { command: "docker", args: ["container", "inspect", "supabase_db_demo"] }, + { command: "podman", args: ["container", "inspect", "supabase_db_demo"] }, + ]); + }); + }); - it.live("forces v9 compat when rest-version reports v9 on a modern database", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-v9-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - "major_version = 15", - `port = ${port}`, - ].join("\n"), - ); - writeTempFile(workdir, "rest-version", "v9.0.1\n"); + it.live("uses sanitized local docker ids and env-backed local db passwords", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-sanitized-")); + writeConfig( + workdir, + [ + 'project_id = "..demo project with spaces"', + "", + "[api]", + 'schemas = ["public"]', + "", + "[db]", + "port = 54322", + ].join("\n"), + ); - const { layer } = setup({ - workdir, - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); + const previousPassword = process.env["SUPABASE_DB_PASSWORD"]; + process.env["SUPABASE_DB_PASSWORD"] = "secret-password"; + const { layer, child, generator } = setup({ workdir }); - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + try { + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); - expect( - docker.env.has("PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=false"), - ).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(child.spawned[0]).toEqual({ + command: "docker", + args: ["container", "inspect", "supabase_db_demo_project_with_spaces"], + }); + expect(generator.calls[0]?.conn.password).toBe("secret-password"); + } finally { + if (previousPassword === undefined) { + delete process.env["SUPABASE_DB_PASSWORD"]; + } else { + process.env["SUPABASE_DB_PASSWORD"] = previousPassword; + } + } + }); + }); - it.live("ignores rest-version v9 marker on databases older than 15", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-pg14-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - "major_version = 14", - `port = ${port}`, - ].join("\n"), - ); - writeTempFile(workdir, "rest-version", "v9.0.1\n"); + it.live("forces v9 compat when rest-version reports v9 on a modern database", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-v9-")); + writeConfig( + workdir, + [ + 'project_id = "demo"', + "", + "[api]", + 'schemas = ["public"]', + "", + "[db]", + "major_version = 15", + "port = 54322", + ].join("\n"), + ); + writeTempFile(workdir, "rest-version", "v9.0.1\n"); - const { layer } = setup({ - workdir, - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); + const { layer, generator } = setup({ workdir }); - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); - expect( - docker.env.has("PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=true"), - ).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(generator.calls[0]?.postgrestV9Compat).toBe(true); + }); + }); - it.live("overrides the pg-meta image version from the pgmeta-version temp file", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-pgmeta-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); - writeTempFile(workdir, "pgmeta-version", "v0.99.0\n"); + it.live("ignores rest-version v9 marker on databases older than 15", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-pg14-")); + writeConfig( + workdir, + [ + 'project_id = "demo"', + "", + "[api]", + 'schemas = ["public"]', + "", + "[db]", + "major_version = 14", + "port = 54322", + ].join("\n"), + ); + writeTempFile(workdir, "rest-version", "v9.0.1\n"); - const { layer, child } = setup({ - workdir, - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); + const { layer, generator } = setup({ workdir }); - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); - expect(child.spawned[1]?.args).toContain(resolvePgmetaImage("0.99.0")); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(generator.calls[0]?.postgrestV9Compat).toBe(false); + }); + }); - it.live("prefers explicit --schema over config schemas for local generation", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-schema-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public", "custom"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); - const { layer } = setup({ workdir, childStdout: ["generated"], onSpawn: docker.onSpawn }); + it.live("prefers explicit --schema over config schemas for local generation", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-schema-")); + writeConfig( + workdir, + [ + 'project_id = "demo"', + "", + "[api]", + 'schemas = ["public", "custom"]', + "", + "[db]", + "port = 54322", + ].join("\n"), + ); + const { layer, generator } = setup({ workdir }); - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true, schema: ["auth", "storage"] })).pipe( - Effect.provide(layer), - ), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ local: true, schema: ["auth", "storage"] })).pipe( + Effect.provide(layer), + ); - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=auth,storage")).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(generator.calls[0]?.includedSchemas).toEqual(["auth", "storage"]); + }); + }); - it.live("falls back to the workdir basename when config has no project_id", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-noid-")); - writeConfig( - workdir, - ["[api]", 'schemas = ["public"]', "", "[db]", `port = ${port}`].join("\n"), - ); - const { layer, child } = setup({ workdir, childStdout: ["generated"] }); + it.live("falls back to the workdir basename when config has no project_id", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-noid-")); + writeConfig(workdir, ["[api]", 'schemas = ["public"]', "", "[db]", "port = 54322"].join("\n")); + const { layer, child } = setup({ workdir }); - await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); - const inspectId = child.spawned[0]?.args[2] ?? ""; - expect(inspectId.startsWith("supabase_db_")).toBe(true); - expect(inspectId).not.toBe("supabase_db_demo"); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + const inspectId = child.spawned[0]?.args[2] ?? ""; + expect(inspectId.startsWith("supabase_db_")).toBe(true); + expect(inspectId).not.toBe("supabase_db_demo"); + }); + }); it.live("generates from --project-id without a local project config", () => { const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-pid-no-config-")); @@ -2888,21 +2283,10 @@ describe("legacy gen types", () => { 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 docker = captureDockerRun(); - const probes: Array<{ host: string; port: number }> = []; - const { layer, out, child } = setup({ + const { layer, out, child, generator } = 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; - }), - }), + generatorOutput: "generated", }); return Effect.gen(function* () { @@ -2913,11 +2297,9 @@ describe("legacy gen types", () => { 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(generator.calls[0]?.conn.host).toBe("127.0.0.1"); + expect(generator.calls[0]?.conn.port).toBe(54322); + expect(generator.calls[0]?.includedSchemas).toEqual(["public", "graphql_public"]); expect(out.stdoutText).toContain("generated"); }); }); @@ -2934,25 +2316,13 @@ describe("legacy gen types", () => { "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({ + const { layer, out, child, generator } = 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; - }), - }), + generatorOutput: "generated", }); return Effect.gen(function* () { @@ -2962,21 +2332,12 @@ describe("legacy gen types", () => { 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(generator.calls[0]?.conn.host).toBe("host.docker.internal"); + expect(generator.calls[0]?.conn.port).toBe(55432); + // SUPABASE_DB_PASSWORD is deliberately excluded when applying project + // env, so the local connection keeps the default password. + expect(generator.calls[0]?.conn.password).toBe("postgres"); + expect(generator.calls[0]?.includedSchemas).toEqual(["public", "private", "graphql_public"]); expect(out.stdoutText).toContain("generated"); }); }); @@ -3005,255 +2366,120 @@ describe("legacy gen types", () => { }); }); - it.live("defaults schemas to public for a db-url run without a project config", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-dburl-no-config-")); - const { layer } = setup({ - workdir, - skipConfig: true, - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - }), - ).pipe(Effect.provide(layer)), - ); - - expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public")).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); - - it.live("surfaces pg-meta container failures after local db inspection succeeds", () => { - return Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-run-error-")); - writeConfig( - workdir, - [ - 'project_id = "demo"', - "", - "[api]", - 'schemas = ["public"]', - "", - "[db]", - `port = ${port}`, - ].join("\n"), - ); - const sequence = mockSequentialChildProcessSpawner([ - { exitCode: 0 }, - { exitCode: 1, stderr: ["pg-meta failed"] }, - ]); - const { layer } = setup({ - workdir, - childLayer: sequence.layer, - }); - - const exit = await Effect.runPromise( - legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer), Effect.exit), - ); - - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(String(exit.cause)).toContain("error running container: exit 1"); - } - expect(sequence.spawned).toHaveLength(2); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }); - }); - - it.live("spawns pg-meta for db-url generation", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer, out, child } = setup({ - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - lang: "swift", - schema: ["public"], - swiftAccessControl: "public", - postgrestV9Compat: true, - queryTimeout: "20s", - }), - ).pipe(Effect.provide(layer)), - ); + it.live("defaults schemas to public for a db-url run without a project config", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-dburl-no-config-")); + const dbUrl = "postgresql://postgres:postgres@127.0.0.1:5432/postgres"; + const { layer, dbConfig, generator } = setup({ + workdir, + skipConfig: true, + }); - expect(out.stderrText).toContain(`Connecting to 127.0.0.1 ${port}`); - expect(child.spawned[0]?.args).toContain("--network"); - expect(child.spawned[0]?.args).toContain("host"); - expect(docker.env.has("PG_META_GENERATE_TYPES=swift")).toBe(true); - expect(docker.env.has("PG_QUERY_TIMEOUT_SECS=20")).toBe(true); - expect( - docker.env.has("PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=false"), - ).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ dbUrl: Option.some(dbUrl) })).pipe( + Effect.provide(layer), + ); - it.live("injects the CA bundle env var when the database speaks TLS", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer } = setup({ - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - schema: ["public"], - }), - ).pipe(Effect.provide(layer)), - ); + expect(dbConfig.resolves).toHaveLength(1); + expect(dbConfig.resolves[0]?.connType).toBe("db-url"); + expect( + dbConfig.resolves[0] !== undefined + ? Option.getOrUndefined(dbConfig.resolves[0].dbUrl) + : undefined, + ).toBe(dbUrl); + expect(generator.calls[0]?.includedSchemas).toEqual(["public"]); + }); + }); - expect(docker.env.startsWith("PG_META_DB_SSL_ROOT_CERT=")).toBe(true); - }, "S"), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + it.live("surfaces generation failures after local db inspection succeeds", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-run-error-")); + writeConfig( + workdir, + ['project_id = "demo"', "", "[api]", 'schemas = ["public"]', "", "[db]", "port = 54322"].join( + "\n", + ), + ); + const { layer, child } = setup({ + workdir, + generatorResults: [ + Effect.fail( + new LegacyGenTypesMetadataError({ + message: "failed to introspect database: relation does not exist", + }), + ), + ], + }); - // The SSL probe does not special-case `--debug`: a successful probe - // returns true regardless, so the bundle is passed to pgmeta regardless of - // the flag. - it.live("passes the CA bundle env var in --debug mode when TLS is supported", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer } = setup({ - childStdout: ["generated"], - debug: true, - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - schema: ["public"], - }), - ).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + const exit = yield* legacyGenTypes(defaultFlags({ local: true })).pipe( + Effect.provide(layer), + Effect.exit, + ); - expect(docker.env.startsWith("PG_META_DB_SSL_ROOT_CERT=")).toBe(true); - }, "S"), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(String(exit.cause)).toContain( + "failed to introspect database: relation does not exist", + ); + } + expect(child.spawned).toHaveLength(1); + }); + }); - it.live("warns on stderr when SUPABASE_CA_SKIP_VERIFY is enabled", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const previous = process.env["SUPABASE_CA_SKIP_VERIFY"]; - process.env["SUPABASE_CA_SKIP_VERIFY"] = "true"; - try { - const { layer, out } = setup({ childStdout: ["generated"] }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - schema: ["public"], - }), - ).pipe(Effect.provide(layer)), - ); + it.live("runs the native generator for db-url generation", () => { + const dbUrl = "postgresql://postgres:postgres@127.0.0.1:5432/postgres"; + const { layer, out, dbConfig, generator, linkedProjectCache } = setup({ + generatorOutput: "generated", + }); - expect(out.stderrText).toContain( - "WARNING: TLS certificate verification disabled for SSL probe (SUPABASE_CA_SKIP_VERIFY=true)", - ); - } finally { - if (previous === undefined) { - delete process.env["SUPABASE_CA_SKIP_VERIFY"]; - } else { - process.env["SUPABASE_CA_SKIP_VERIFY"] = previous; - } - } + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(dbUrl), + lang: "swift", + schema: ["public"], + swiftAccessControl: "public", + postgrestV9Compat: true, + queryTimeout: "20s", }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + ).pipe(Effect.provide(layer)); - it.live("honors the --network-id override for the db-url connection", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer, child } = setup({ - childStdout: ["generated"], - networkId: Option.some("custom-network"), - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - schema: ["public"], - }), - ).pipe(Effect.provide(layer)), - ); + expect(out.stderrText).toContain("Connecting to 127.0.0.1 5432"); + expect(dbConfig.resolves).toHaveLength(1); + expect(dbConfig.resolves[0]?.connType).toBe("db-url"); + expect(generator.calls[0]?.lang).toBe("swift"); + expect(generator.calls[0]?.swiftAccessControl).toBe("public"); + expect(generator.calls[0]?.postgrestV9Compat).toBe(true); + expect(generator.calls[0]?.queryTimeoutSeconds).toBe(20); + expect(generator.calls[0]?.isLocal).toBe(false); + expect(out.stdoutText).toBe("generated\n"); + expect(linkedProjectCache.cached).toBe(false); + }); + }); - expect(child.spawned[0]?.args).toContain("custom-network"); - expect(child.spawned[0]?.args).not.toContain("host"); + it.live("passes the resolver's local detection through for a local db-url", () => { + const dbUrl = "postgresql://postgres@127.0.0.1:54322/postgres"; + const { layer, generator } = setup({ + dbConfigResolve: () => + Effect.succeed({ + conn: { + host: "127.0.0.1", + port: 54322, + user: "postgres", + password: "postgres", + database: "postgres", + }, + isLocal: true, }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + }); - it.live("defaults bare db-url connections to the postgres database", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer } = setup({ - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}`), - lang: "swift", - schema: ["public"], - swiftAccessControl: "public", - postgrestV9Compat: true, - queryTimeout: "20s", - }), - ).pipe(Effect.provide(layer)), - ); + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ dbUrl: Option.some(dbUrl) })).pipe( + Effect.provide(layer), + ); - expect( - docker.env.has( - `PG_META_DB_URL=postgresql://postgres:postgres@127.0.0.1:${port}/postgres`, - ), - ).toBe(true); - }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + expect(generator.calls[0]?.isLocal).toBe(true); + }); + }); it.live("accepts legacy positional typescript without changing behavior", () => { const { layer } = setup({ @@ -3301,30 +2527,22 @@ describe("legacy gen types", () => { }, ); - it.live("allows legacy positional non-typescript when --lang is explicitly set", () => - Effect.tryPromise({ - try: () => - withSslProbeServer(async (port) => { - const docker = captureDockerRun(); - const { layer } = setup({ - args: ["gen", "types", "go", "--lang", "go"], - childStdout: ["generated"], - onSpawn: docker.onSpawn, - }); - - await Effect.runPromise( - legacyGenTypes( - defaultFlags({ - dbUrl: Option.some(`postgresql://postgres:postgres@127.0.0.1:${port}/postgres`), - lang: "go", - schema: ["public"], - }), - ).pipe(Effect.provide(layer)), - ); + it.live("allows legacy positional non-typescript when --lang is explicitly set", () => { + const dbUrl = "postgresql://postgres:postgres@127.0.0.1:5432/postgres"; + const { layer, generator } = setup({ + args: ["gen", "types", "go", "--lang", "go"], + }); - expect(docker.env.has("PG_META_GENERATE_TYPES=go")).toBe(true); + return Effect.gen(function* () { + yield* legacyGenTypes( + defaultFlags({ + dbUrl: Option.some(dbUrl), + lang: "go", + schema: ["public"], }), - catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), - }), - ); + ).pipe(Effect.provide(layer)); + + expect(generator.calls[0]?.lang).toBe("go"); + }); + }); }); diff --git a/apps/cli/src/legacy/commands/gen/types/types.layers.ts b/apps/cli/src/legacy/commands/gen/types/types.layers.ts index 70256eddc0..e1dc51d21f 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.layers.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.layers.ts @@ -12,7 +12,6 @@ import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service import { legacyDbConnectionLayer } from "../../../shared/legacy-db-connection.layer.ts"; import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts"; import { legacyPgDeltaSslProbeLayer } from "../../../shared/legacy-pgdelta-ssl-probe.layer.ts"; -import { LegacyPgDeltaSslProbe } from "../../../shared/legacy-pgdelta-ssl-probe.service.ts"; import { LegacyIdentityStitch, legacyIdentityStitchLayer, @@ -24,6 +23,8 @@ import { legacyTelemetryStateLayer } from "../../../telemetry/legacy-telemetry-s import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; import { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts"; import { CommandRuntime } from "../../../../shared/runtime/command-runtime.service.ts"; +import { legacyGenTypesGeneratorLayer } from "./types.generator.layer.ts"; +import { LegacyGenTypesGenerator } from "./types.generator.ts"; /** * `gen types --local` and `--db-url` do not use the Management API, so this @@ -66,7 +67,7 @@ export const legacyGenTypesRuntimeLayer = (() => { Layer.provide(httpClient), Layer.provide(legacyIdentityStitchLayer), ), - legacyPgDeltaSslProbeLayer, + legacyGenTypesGeneratorLayer.pipe(Layer.provide(legacyPgDeltaSslProbeLayer)), legacyTelemetryStateLayer, // The one per-command identity stitcher, exposed at top level so // `withLegacyCommandInstrumentation` can read @@ -91,7 +92,7 @@ type LegacyGenTypesServices = | LegacyCliSettings | LegacyProjectRefResolver | LegacyDbConfigResolver - | LegacyPgDeltaSslProbe + | LegacyGenTypesGenerator | LegacyLinkedProjectCache | LegacyTelemetryState | LegacyIdentityStitch diff --git a/apps/cli/src/legacy/commands/gen/types/types.oxfmt.ts b/apps/cli/src/legacy/commands/gen/types/types.oxfmt.ts new file mode 100644 index 0000000000..5841dfa2bf --- /dev/null +++ b/apps/cli/src/legacy/commands/gen/types/types.oxfmt.ts @@ -0,0 +1,107 @@ +/** + * Statically-dispatched oxfmt native binding for TypeScript typegen output. + * + * `@supabase/postgrest-typegen`'s default formatter goes through the `oxfmt` + * JS package, whose ESM dist resolves its platform binding at runtime via + * `createRequire(import.meta.url)` — a dynamic path `bun build --compile` + * cannot follow, so the compiled binary would fail to find the `.node` addon + * (and the same dist lazily imports optional prettier plugins that are not + * installed at all). Instead, mirror the `@parcel/watcher` pattern + * (`shared/runtime/parcel-file-watcher.layer.ts`): one static `require` per + * shipped CLI target, so Bun embeds exactly the right `.node` binding, and + * inject the resulting formatter through the generator's `format` option. + * + * The binding version in `package.json` must stay in lockstep with the + * `oxfmt` version pinned by `@supabase/postgrest-typegen`, and the format + * options below must mirror the package's own `defaultFormat` so injected and + * default output stay identical. + */ + +declare const SUPABASE_LIBC: string | undefined; + +/** + * Callback the binding invokes to format embedded languages (CSS-in-JS + * template literals and similar). Generated type declarations contain no + * template literals, so these can never fire for typegen output. + */ +type LegacyOxfmtEmbedCallback = (options: unknown, code: unknown) => never; + +interface LegacyOxfmtBinding { + readonly format: ( + fileName: string, + sourceText: string, + options: Readonly>, + formatFileCallback: LegacyOxfmtEmbedCallback, + formatEmbeddedCodeCallback: LegacyOxfmtEmbedCallback, + formatEmbeddedDocCallback: LegacyOxfmtEmbedCallback, + ) => Promise<{ + readonly code: string; + readonly errors: ReadonlyArray<{ readonly message: string }>; + }>; +} + +function legacyRequireOxfmtBinding(): LegacyOxfmtBinding { + if (process.platform === "darwin") { + if (process.arch === "arm64") { + return require("@oxfmt/binding-darwin-arm64"); + } + if (process.arch === "x64") { + return require("@oxfmt/binding-darwin-x64"); + } + } + + if (process.platform === "linux") { + if (process.arch === "arm64") { + if (typeof SUPABASE_LIBC !== "undefined" && SUPABASE_LIBC === "musl") { + return require("@oxfmt/binding-linux-arm64-musl"); + } + return require("@oxfmt/binding-linux-arm64-gnu"); + } + if (process.arch === "x64") { + if (typeof SUPABASE_LIBC !== "undefined" && SUPABASE_LIBC === "musl") { + return require("@oxfmt/binding-linux-x64-musl"); + } + return require("@oxfmt/binding-linux-x64-gnu"); + } + } + + if (process.platform === "win32") { + if (process.arch === "arm64") { + return require("@oxfmt/binding-win32-arm64-msvc"); + } + if (process.arch === "x64") { + return require("@oxfmt/binding-win32-x64-msvc"); + } + } + + throw new Error(`Unsupported oxfmt platform: ${process.platform}-${process.arch}`); +} + +const rejectEmbedded: LegacyOxfmtEmbedCallback = () => { + throw new Error("embedded-language formatting is not available for generated types"); +}; + +/** + * Drop-in for `GenerateTypescriptOptions.format`, byte-equivalent to the + * typegen package's own oxfmt default (same virtual file name, same + * `semi`/`printWidth` options, same error surfacing). + */ +export async function legacyOxfmtTypegenFormat(code: string): Promise { + const binding = legacyRequireOxfmtBinding(); + const { code: formatted, errors } = await binding.format( + "output.ts", + code, + { semi: false, printWidth: 80 }, + rejectEmbedded, + rejectEmbedded, + rejectEmbedded, + ); + if (errors.length > 0) { + throw new Error( + `oxfmt failed to format generated TypeScript output: ${errors + .map((error) => error.message) + .join("; ")}`, + ); + } + return formatted; +} 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 c94d53665f..f0b44cfda0 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.shared.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.shared.ts @@ -1,20 +1,11 @@ import { Effect } from "effect"; -import { dockerfileServiceImageRaw } from "../../../../shared/services/dockerfile-images.ts"; -import { slimImageForCurrentPin } from "../../../../shared/services/slim-images.ts"; -import { legacyGetRegistryImageUrl } from "../../../shared/legacy-docker-registry.ts"; -import { - LegacyInvalidGenTypesDatabaseUrlError, - LegacyInvalidGenTypesDurationError, -} from "./types.errors.ts"; -import caProd2021 from "./templates/prod-ca-2021.ts"; -import caProd2025 from "./templates/prod-ca-2025.ts"; -import caStaging2021 from "./templates/staging-ca-2021.ts"; -// Local Docker resource ids are hoisted to `legacy/shared` so the declarative seam -// can derive the same `supabase_db_` name when checking the local stack. -export { localDbContainerId, localNetworkId } from "../../../shared/legacy-docker-ids.ts"; +import type { LegacyPgConnInput } from "../../../shared/legacy-db-connection.service.ts"; +import { LegacyInvalidGenTypesDurationError } from "./types.errors.ts"; -const LEGACY_DEFAULT_CONNECT_TIMEOUT_SECONDS = 10; +// The local Docker container id is hoisted to `legacy/shared` so the declarative +// seam can derive the same `supabase_db_` name when checking the local stack. +export { localDbContainerId } from "../../../shared/legacy-docker-ids.ts"; const DURATION_UNITS_TO_MILLIS = { ns: 1 / 1_000_000, @@ -32,13 +23,6 @@ const DURATION_PART_PATTERN = new RegExp( "g", ); -export interface LegacyGenTypesDbTarget { - readonly url: string; - readonly host: string; - readonly port: number; - readonly networkMode: "host" | (string & {}); -} - export function defaultSchemas(extraSchemas: ReadonlyArray = []) { return [...new Set(["public", ...extraSchemas])]; } @@ -98,55 +82,42 @@ export function localDbPassword() { return process.env["SUPABASE_DB_PASSWORD"] ?? "postgres"; } -export function parseDatabaseUrl( - url: string, -): Effect.Effect { - return Effect.try({ - try: () => { - const parsed = new URL(url); - if (parsed.protocol !== "postgresql:" && parsed.protocol !== "postgres:") { - throw new Error(`unsupported scheme ${parsed.protocol}`); - } - if (parsed.pathname.length === 0 || parsed.pathname === "/") { - parsed.pathname = "/postgres"; - } - return { - url: parsed.toString(), - host: parsed.hostname, - port: parsed.port.length > 0 ? Number.parseInt(parsed.port, 10) : 5432, - networkMode: "host" as const, - } satisfies LegacyGenTypesDbTarget; - }, - catch: (cause) => - new LegacyInvalidGenTypesDatabaseUrlError({ - message: `failed to parse connection string: ${cause instanceof Error ? cause.message : String(cause)}`, - }), - }); -} - -export function buildPostgresUrl(input: { - readonly host: string; - readonly port: number; - readonly user: string; - readonly password: string; - readonly database: string; -}) { - const host = - input.host.includes(":") && !input.host.startsWith("[") ? `[${input.host}]` : input.host; - return ( - `postgresql://${encodeURIComponent(input.user)}:${encodeURIComponent(input.password)}` + - `@${host}:${input.port}/${encodeURIComponent(input.database)}` + - `?connect_timeout=${LEGACY_DEFAULT_CONNECT_TIMEOUT_SECONDS}` - ); -} - -export function resolvePgmetaImage(versionOverride?: string) { - const raw = dockerfileServiceImageRaw("pgmeta"); - const trimmed = versionOverride?.trim() ?? ""; - const pin = trimmed.length > 0 ? `v${trimmed.replace(/^v/i, "")}` : undefined; - return legacyGetRegistryImageUrl(slimImageForCurrentPin("pgmeta", raw, pin)); +/** + * `--query-timeout` parity with the retired pg-meta envs: the flag becomes + * session `statement_timeout` (milliseconds; `0` disables) and, when the DSN + * has no `connect_timeout` and the flag is positive, the connect timeout. + * Zero must not become `connectTimeoutSeconds: 0` — the driver treats that as + * an immediate `Effect.timeout` rather than "disabled". + */ +export function applyQueryTimeouts( + conn: LegacyPgConnInput, + queryTimeoutSeconds: number, +): LegacyPgConnInput { + const runtimeParams = { + ...conn.runtimeParams, + statement_timeout: `${queryTimeoutSeconds * 1000}`, + }; + if (queryTimeoutSeconds > 0 && conn.connectTimeoutSeconds === undefined) { + return { ...conn, connectTimeoutSeconds: queryTimeoutSeconds, runtimeParams }; + } + return { ...conn, runtimeParams }; } -export function legacyRootCaBundle() { - return `${caStaging2021}${caProd2021}${caProd2025}`; +/** + * When the DSN omitted `sslmode`, the SSLRequest probe decides: no TLS → + * `disable`; TLS → `require` plus the embedded CA path so the driver promotes + * to `verify-ca` (the retired `PG_META_DB_SSL_ROOT_CERT` injection). + */ +export function applyProbedSslMode( + conn: LegacyPgConnInput, + useTls: boolean, + sslrootcert?: string, +): LegacyPgConnInput { + if (conn.sslmode !== undefined) return conn; + if (!useTls) return { ...conn, sslmode: "disable" }; + return { + ...conn, + sslmode: "require", + ...(sslrootcert !== undefined && sslrootcert.length > 0 ? { sslrootcert } : {}), + }; } diff --git a/apps/cli/src/legacy/commands/gen/types/types.unit.test.ts b/apps/cli/src/legacy/commands/gen/types/types.unit.test.ts index f125292570..c02953cbf3 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.unit.test.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.unit.test.ts @@ -1,23 +1,23 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Exit } from "effect"; -import { dockerfileServiceImageRaw } from "../../../../shared/services/dockerfile-images.ts"; -import { toSlimImage } from "../../../../shared/services/slim-images.ts"; import { legacyGetHostname } from "../../../shared/legacy-hostname.ts"; import { legacyParseSchemaFlags } from "../../../shared/legacy-schema-flags.ts"; import { - buildPostgresUrl, + applyProbedSslMode, + applyQueryTimeouts, defaultSchemas, - legacyRootCaBundle, localDbContainerId, localDbPassword, - localNetworkId, - parseDatabaseUrl, parseQueryTimeoutSeconds, - resolvePgmetaImage, } from "./types.shared.ts"; -const currentPgmeta = dockerfileServiceImageRaw("pgmeta"); -const currentPgmetaTag = currentPgmeta.split(":")[1] ?? ""; +const BASE_CONN = { + host: "db.example.com", + port: 5432, + user: "postgres", + password: "secret", + database: "postgres", +}; function withEnv(key: string, value: string | undefined, run: () => T): T { const previous = process.env[key]; @@ -90,116 +90,46 @@ describe("parseQueryTimeoutSeconds", () => { ); }); -describe("parseDatabaseUrl", () => { - it.effect("parses a full postgresql url", () => - Effect.gen(function* () { - const result = yield* parseDatabaseUrl("postgresql://user:pw@example.com:6543/mydb"); - expect(result.host).toBe("example.com"); - expect(result.port).toBe(6543); - expect(result.networkMode).toBe("host"); - expect(result.url).toContain("/mydb"); - }), - ); - - it.effect("accepts the postgres:// scheme and defaults the database", () => - Effect.gen(function* () { - const result = yield* parseDatabaseUrl("postgres://user:pw@example.com/"); - expect(result.url).toContain("/postgres"); - }), - ); - - it.effect("defaults the port to 5432 when omitted", () => - Effect.gen(function* () { - const result = yield* parseDatabaseUrl("postgresql://user:pw@example.com/db"); - expect(result.port).toBe(5432); - }), - ); - - it.effect("rejects an unsupported scheme", () => - Effect.gen(function* () { - const exit = yield* parseDatabaseUrl("mysql://user:pw@example.com/db").pipe(Effect.exit); - expect(Exit.isFailure(exit)).toBe(true); - }), - ); - - it.effect("rejects a malformed connection string", () => - Effect.gen(function* () { - const exit = yield* parseDatabaseUrl("not a url").pipe(Effect.exit); - expect(Exit.isFailure(exit)).toBe(true); - }), - ); -}); - -describe("resolvePgmetaImage", () => { - it("uses the default pgmeta version when no override is given", () => { - const image = withEnv("SUPABASE_USE_SLIM_IMAGES", undefined, () => - withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", undefined, () => resolvePgmetaImage()), +describe("applyQueryTimeouts", () => { + it("writes statement_timeout last so the flag overrides a DSN value", () => { + const conn = applyQueryTimeouts( + { ...BASE_CONN, runtimeParams: { statement_timeout: "0", search_path: "public" } }, + 15, ); - expect(image).toContain("postgres-meta"); - }); - - it("strips a leading v from a version override", () => { - const image = withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", "docker.io", () => - resolvePgmetaImage("v1.2.3"), - ); - expect(image).toBe("supabase/postgres-meta:v1.2.3"); - }); - - it("falls back to the default when the override is blank", () => { - const withOverride = withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", "docker.io", () => - resolvePgmetaImage(" "), - ); - const withoutOverride = withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", "docker.io", () => - resolvePgmetaImage(), - ); - expect(withOverride).toBe(withoutOverride); - }); - - it("uses the supabase registry for any non docker.io registry", () => { - const image = withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", undefined, () => - resolvePgmetaImage("1.2.3"), - ); - expect(image).not.toBe("supabase/postgres-meta:v1.2.3"); - expect(image).toContain("postgres-meta:v1.2.3"); + expect(conn.runtimeParams).toEqual({ + statement_timeout: "15000", + search_path: "public", + }); + expect(conn.connectTimeoutSeconds).toBe(15); }); - it("defaults to the ECR mirror when no registry override is set", () => { - const image = withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", undefined, () => - resolvePgmetaImage("1.2.3"), - ); - expect(image).toBe("public.ecr.aws/supabase/postgres-meta:v1.2.3"); + it("leaves connect timeout unset when the query timeout is zero", () => { + const conn = applyQueryTimeouts(BASE_CONN, 0); + expect(conn.connectTimeoutSeconds).toBeUndefined(); + expect(conn.runtimeParams).toEqual({ statement_timeout: "0" }); }); - it("honors SUPABASE_INTERNAL_IMAGE_REGISTRY for a non docker.io registry (e.g. ghcr.io)", () => { - // Regression: setup-cli exports `ghcr.io` on shared CI runners to dodge ECR - // rate limits, but gen types used to ignore it and still pull from ECR. - const image = withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", "ghcr.io", () => - resolvePgmetaImage("1.2.3"), - ); - expect(image).toBe("ghcr.io/supabase/postgres-meta:v1.2.3"); + it("keeps an explicit DSN connect_timeout", () => { + const conn = applyQueryTimeouts({ ...BASE_CONN, connectTimeoutSeconds: 30 }, 15); + expect(conn.connectTimeoutSeconds).toBe(30); }); +}); - it("rewrites to an arbitrary configured mirror registry", () => { - const image = withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", "my.registry.example", () => - resolvePgmetaImage("1.2.3"), - ); - expect(image).toBe("my.registry.example/supabase/postgres-meta:v1.2.3"); +describe("applyProbedSslMode", () => { + it("disables TLS when the probe reports a plain-TCP server", () => { + expect(applyProbedSslMode(BASE_CONN, false).sslmode).toBe("disable"); }); - it("slim-translates the current pin and skips registry rewrite", () => { - const image = withEnv("SUPABASE_USE_SLIM_IMAGES", "1", () => - withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", undefined, () => - resolvePgmetaImage(currentPgmetaTag), - ), - ); - expect(image).toBe(toSlimImage("pgmeta", currentPgmeta)); + it("pins require plus the CA path when the probe reports TLS", () => { + expect(applyProbedSslMode(BASE_CONN, true, "/tmp/root.crt")).toMatchObject({ + sslmode: "require", + sslrootcert: "/tmp/root.crt", + }); }); - it("keeps a historical pg-meta pin on docker.io under the slim flag", () => { - const image = withEnv("SUPABASE_USE_SLIM_IMAGES", "1", () => - withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", "docker.io", () => resolvePgmetaImage("1.2.3")), - ); - expect(image).toBe("supabase/postgres-meta:v1.2.3"); + it("leaves an explicit sslmode unchanged", () => { + const conn = { ...BASE_CONN, sslmode: "verify-full" }; + expect(applyProbedSslMode(conn, true, "/tmp/root.crt")).toBe(conn); }); }); @@ -222,7 +152,6 @@ describe("schema and id helpers", () => { it("derives sanitized docker ids from the project id", () => { expect(localDbContainerId("..my project")).toBe("supabase_db_my_project"); - expect(localNetworkId("..my project")).toBe("supabase_network_my_project"); }); it("truncates an over-long project id to 40 characters", () => { @@ -242,19 +171,4 @@ describe("schema and id helpers", () => { expect(withEnv("SUPABASE_DB_PASSWORD", undefined, () => localDbPassword())).toBe("postgres"); expect(withEnv("SUPABASE_DB_PASSWORD", "secret", () => localDbPassword())).toBe("secret"); }); - - it("brackets ipv6 hosts in the generated postgres url", () => { - const url = buildPostgresUrl({ - host: "::1", - port: 5432, - user: "postgres", - password: "pw", - database: "postgres", - }); - expect(url).toContain("@[::1]:5432/"); - }); - - it("bundles the staging and production CA certificates", () => { - expect(legacyRootCaBundle().length).toBeGreaterThan(0); - }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef66766800..61d0c94157 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,30 @@ importers: '@napi-rs/keyring': specifier: ^1.3.0 version: 1.3.0 + '@oxfmt/binding-darwin-arm64': + specifier: 0.65.0 + version: 0.65.0 + '@oxfmt/binding-darwin-x64': + specifier: 0.65.0 + version: 0.65.0 + '@oxfmt/binding-linux-arm64-gnu': + specifier: 0.65.0 + version: 0.65.0 + '@oxfmt/binding-linux-arm64-musl': + specifier: 0.65.0 + version: 0.65.0 + '@oxfmt/binding-linux-x64-gnu': + specifier: 0.65.0 + version: 0.65.0 + '@oxfmt/binding-linux-x64-musl': + specifier: 0.65.0 + version: 0.65.0 + '@oxfmt/binding-win32-arm64-msvc': + specifier: 0.65.0 + version: 0.65.0 + '@oxfmt/binding-win32-x64-msvc': + specifier: 0.65.0 + version: 0.65.0 '@parcel/watcher': specifier: ^2.6.0 version: 2.6.0 @@ -180,6 +204,9 @@ importers: '@supabase/pg-topo': specifier: 1.0.0-alpha.5 version: 1.0.0-alpha.5 + '@supabase/postgrest-typegen': + specifier: 0.2.0 + version: 0.2.0 '@supabase/process-compose': specifier: workspace:* version: link:../../packages/process-compose @@ -588,6 +615,12 @@ packages: zod: optional: true + '@ark/schema@0.56.2': + resolution: {integrity: sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==} + + '@ark/util@0.56.2': + resolution: {integrity: sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -1673,42 +1706,84 @@ packages: cpu: [arm] os: [android] + '@oxfmt/binding-android-arm-eabi@0.65.0': + resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxfmt/binding-android-arm64@0.63.0': resolution: {integrity: sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@oxfmt/binding-android-arm64@0.65.0': + resolution: {integrity: sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxfmt/binding-darwin-arm64@0.63.0': resolution: {integrity: sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@oxfmt/binding-darwin-arm64@0.65.0': + resolution: {integrity: sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxfmt/binding-darwin-x64@0.63.0': resolution: {integrity: sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@oxfmt/binding-darwin-x64@0.65.0': + resolution: {integrity: sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxfmt/binding-freebsd-x64@0.63.0': resolution: {integrity: sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@oxfmt/binding-freebsd-x64@0.65.0': + resolution: {integrity: sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxfmt/binding-linux-arm-gnueabihf@0.63.0': resolution: {integrity: sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': + resolution: {integrity: sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxfmt/binding-linux-arm-musleabihf@0.63.0': resolution: {integrity: sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': + resolution: {integrity: sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxfmt/binding-linux-arm64-gnu@0.63.0': resolution: {integrity: sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1716,6 +1791,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-arm64-gnu@0.65.0': + resolution: {integrity: sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-arm64-musl@0.63.0': resolution: {integrity: sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1723,6 +1805,13 @@ packages: os: [linux] libc: [musl] + '@oxfmt/binding-linux-arm64-musl@0.65.0': + resolution: {integrity: sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@oxfmt/binding-linux-ppc64-gnu@0.63.0': resolution: {integrity: sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1730,6 +1819,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': + resolution: {integrity: sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-riscv64-gnu@0.63.0': resolution: {integrity: sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1737,6 +1833,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': + resolution: {integrity: sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-riscv64-musl@0.63.0': resolution: {integrity: sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1744,6 +1847,13 @@ packages: os: [linux] libc: [musl] + '@oxfmt/binding-linux-riscv64-musl@0.65.0': + resolution: {integrity: sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxfmt/binding-linux-s390x-gnu@0.63.0': resolution: {integrity: sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1751,6 +1861,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-s390x-gnu@0.65.0': + resolution: {integrity: sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-x64-gnu@0.63.0': resolution: {integrity: sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1758,6 +1875,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-x64-gnu@0.65.0': + resolution: {integrity: sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-x64-musl@0.63.0': resolution: {integrity: sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1765,30 +1889,61 @@ packages: os: [linux] libc: [musl] + '@oxfmt/binding-linux-x64-musl@0.65.0': + resolution: {integrity: sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxfmt/binding-openharmony-arm64@0.63.0': resolution: {integrity: sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@oxfmt/binding-openharmony-arm64@0.65.0': + resolution: {integrity: sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxfmt/binding-win32-arm64-msvc@0.63.0': resolution: {integrity: sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@oxfmt/binding-win32-arm64-msvc@0.65.0': + resolution: {integrity: sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxfmt/binding-win32-ia32-msvc@0.63.0': resolution: {integrity: sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@oxfmt/binding-win32-ia32-msvc@0.65.0': + resolution: {integrity: sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxfmt/binding-win32-x64-msvc@0.63.0': resolution: {integrity: sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@oxfmt/binding-win32-x64-msvc@0.65.0': + resolution: {integrity: sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxlint-tsgolint/darwin-arm64@7.0.2001': resolution: {integrity: sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==} cpu: [arm64] @@ -2685,6 +2840,10 @@ packages: resolution: {integrity: sha512-+Mf6uCpzr00bqxwX8hTK2X2L9eAL/1vuOjdEjx6upz9ulb0RmQT16XeU/JkMUlVHw/B46ZnPa2busY4Kd9YCzw==} engines: {node: '>=22.0.0'} + '@supabase/postgrest-typegen@0.2.0': + resolution: {integrity: sha512-y+dQsjV0D9IVQ2wW0WBl48owyD/88X8dh78XE4rSo1s8MegCOc5/ZNAOkBVICwTySm/hIk2Iw+0zPqJiEh0XQg==} + engines: {node: '>=20.0.0'} + '@supabase/realtime-js@2.112.3': resolution: {integrity: sha512-E6wljXWs7DUOloyIB69i3YFInWE6IyCvgTAbQ0KYxOHv26FdA1KzEXTuzxrYEdf70t406Z9BRwUlGyclGF2FXA==} engines: {node: '>=22.0.0'} @@ -3204,6 +3363,12 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + arkregex@0.0.8: + resolution: {integrity: sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==} + + arktype@2.2.3: + resolution: {integrity: sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -5335,6 +5500,19 @@ packages: vite-plus: optional: true + oxfmt@0.65.0: + resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + oxlint-tsgolint@7.0.2001: resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true @@ -5479,6 +5657,10 @@ packages: peerDependencies: pg: ^8 + pg-format@1.0.4: + resolution: {integrity: sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==} + engines: {node: '>=4.0'} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -6801,6 +6983,12 @@ snapshots: optionalDependencies: zod: 4.4.3 + '@ark/schema@0.56.2': + dependencies: + '@ark/util': 0.56.2 + + '@ark/util@0.56.2': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -7704,60 +7892,109 @@ snapshots: '@oxfmt/binding-android-arm-eabi@0.63.0': optional: true + '@oxfmt/binding-android-arm-eabi@0.65.0': + optional: true + '@oxfmt/binding-android-arm64@0.63.0': optional: true + '@oxfmt/binding-android-arm64@0.65.0': + optional: true + '@oxfmt/binding-darwin-arm64@0.63.0': optional: true + '@oxfmt/binding-darwin-arm64@0.65.0': {} + '@oxfmt/binding-darwin-x64@0.63.0': optional: true + '@oxfmt/binding-darwin-x64@0.65.0': {} + '@oxfmt/binding-freebsd-x64@0.63.0': optional: true + '@oxfmt/binding-freebsd-x64@0.65.0': + optional: true + '@oxfmt/binding-linux-arm-gnueabihf@0.63.0': optional: true + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': + optional: true + '@oxfmt/binding-linux-arm-musleabihf@0.63.0': optional: true + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': + optional: true + '@oxfmt/binding-linux-arm64-gnu@0.63.0': optional: true + '@oxfmt/binding-linux-arm64-gnu@0.65.0': {} + '@oxfmt/binding-linux-arm64-musl@0.63.0': optional: true + '@oxfmt/binding-linux-arm64-musl@0.65.0': {} + '@oxfmt/binding-linux-ppc64-gnu@0.63.0': optional: true + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': + optional: true + '@oxfmt/binding-linux-riscv64-gnu@0.63.0': optional: true + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': + optional: true + '@oxfmt/binding-linux-riscv64-musl@0.63.0': optional: true + '@oxfmt/binding-linux-riscv64-musl@0.65.0': + optional: true + '@oxfmt/binding-linux-s390x-gnu@0.63.0': optional: true + '@oxfmt/binding-linux-s390x-gnu@0.65.0': + optional: true + '@oxfmt/binding-linux-x64-gnu@0.63.0': optional: true + '@oxfmt/binding-linux-x64-gnu@0.65.0': {} + '@oxfmt/binding-linux-x64-musl@0.63.0': optional: true + '@oxfmt/binding-linux-x64-musl@0.65.0': {} + '@oxfmt/binding-openharmony-arm64@0.63.0': optional: true + '@oxfmt/binding-openharmony-arm64@0.65.0': + optional: true + '@oxfmt/binding-win32-arm64-msvc@0.63.0': optional: true + '@oxfmt/binding-win32-arm64-msvc@0.65.0': {} + '@oxfmt/binding-win32-ia32-msvc@0.63.0': optional: true + '@oxfmt/binding-win32-ia32-msvc@0.65.0': + optional: true + '@oxfmt/binding-win32-x64-msvc@0.63.0': optional: true + '@oxfmt/binding-win32-x64-msvc@0.65.0': {} + '@oxlint-tsgolint/darwin-arm64@7.0.2001': optional: true @@ -8515,6 +8752,15 @@ snapshots: dependencies: tslib: 2.8.1 + '@supabase/postgrest-typegen@0.2.0': + dependencies: + arktype: 2.2.3 + oxfmt: 0.65.0 + pg-format: 1.0.4 + transitivePeerDependencies: + - svelte + - vite-plus + '@supabase/realtime-js@2.112.3': dependencies: '@supabase/phoenix': 0.4.5 @@ -9039,6 +9285,16 @@ snapshots: dependencies: tslib: 2.8.1 + arkregex@0.0.8: + dependencies: + '@ark/util': 0.56.2 + + arktype@2.2.3: + dependencies: + '@ark/schema': 0.56.2 + '@ark/util': 0.56.2 + arkregex: 0.0.8 + array-flatten@1.1.1: {} array-ify@1.0.0: {} @@ -11472,6 +11728,30 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.63.0 '@oxfmt/binding-win32-x64-msvc': 0.63.0 + oxfmt@0.65.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.65.0 + '@oxfmt/binding-android-arm64': 0.65.0 + '@oxfmt/binding-darwin-arm64': 0.65.0 + '@oxfmt/binding-darwin-x64': 0.65.0 + '@oxfmt/binding-freebsd-x64': 0.65.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.65.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.65.0 + '@oxfmt/binding-linux-arm64-gnu': 0.65.0 + '@oxfmt/binding-linux-arm64-musl': 0.65.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-musl': 0.65.0 + '@oxfmt/binding-linux-s390x-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-musl': 0.65.0 + '@oxfmt/binding-openharmony-arm64': 0.65.0 + '@oxfmt/binding-win32-arm64-msvc': 0.65.0 + '@oxfmt/binding-win32-ia32-msvc': 0.65.0 + '@oxfmt/binding-win32-x64-msvc': 0.65.0 + oxlint-tsgolint@7.0.2001: optionalDependencies: '@oxlint-tsgolint/darwin-arm64': 7.0.2001 @@ -11615,6 +11895,8 @@ snapshots: dependencies: pg: 8.23.0 + pg-format@1.0.4: {} + pg-int8@1.0.1: {} pg-numeric@1.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c816e33358..e71492de8e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -53,6 +53,27 @@ minimumReleaseAgeExclude: - "@effect/vitest@4.0.0-rc.111" - "@supabase/pg-delta@1.0.0-alpha.46" - "@supabase/pg-topo@1.0.0-alpha.5" + - "@supabase/postgrest-typegen@0.2.0" + - "oxfmt@0.65.0" + - "@oxfmt/binding-android-arm-eabi@0.65.0" + - "@oxfmt/binding-android-arm64@0.65.0" + - "@oxfmt/binding-darwin-arm64@0.65.0" + - "@oxfmt/binding-darwin-x64@0.65.0" + - "@oxfmt/binding-freebsd-x64@0.65.0" + - "@oxfmt/binding-linux-arm-gnueabihf@0.65.0" + - "@oxfmt/binding-linux-arm-musleabihf@0.65.0" + - "@oxfmt/binding-linux-arm64-gnu@0.65.0" + - "@oxfmt/binding-linux-arm64-musl@0.65.0" + - "@oxfmt/binding-linux-ppc64-gnu@0.65.0" + - "@oxfmt/binding-linux-riscv64-gnu@0.65.0" + - "@oxfmt/binding-linux-riscv64-musl@0.65.0" + - "@oxfmt/binding-linux-s390x-gnu@0.65.0" + - "@oxfmt/binding-linux-x64-gnu@0.65.0" + - "@oxfmt/binding-linux-x64-musl@0.65.0" + - "@oxfmt/binding-openharmony-arm64@0.65.0" + - "@oxfmt/binding-win32-arm64-msvc@0.65.0" + - "@oxfmt/binding-win32-ia32-msvc@0.65.0" + - "@oxfmt/binding-win32-x64-msvc@0.65.0" - "@types/bun@1.4.0" - "bun-types@1.4.0" - "effect@4.0.0-rc.111"