Skip to content

feat(postgrest-typegen): add OpenAPI producer for credential-free type generation#303

Open
avallete wants to merge 1 commit into
claude/funny-tesla-wofjdjfrom
avallete/zen-colden-fa586b
Open

feat(postgrest-typegen): add OpenAPI producer for credential-free type generation#303
avallete wants to merge 1 commit into
claude/funny-tesla-wofjdjfrom
avallete/zen-colden-fa586b

Conversation

@avallete

@avallete avallete commented Jun 20, 2026

Copy link
Copy Markdown
Member

What

Adds @supabase/postgrest-typegen/openapi — a second GeneratorMetadata producer alongside introspect(db):

import { openApiToGeneratorMetadata } from "@supabase/postgrest-typegen/openapi";
const metadata = openApiToGeneratorMetadata(await (await fetch(url)).json());
const types = await generateTypescript(sortGeneratorMetadata(metadata));

It maps a PostgREST OpenAPI document's x-postgrest-typegen-metadata vendor extension to GeneratorMetadata, so language types can be generated from a PostgREST URL alone — no database connection.

Why

A feasibility spike (experiments/openapi-spike/REPORT.md) showed plain PostgREST OpenAPI is too lossy for faithful type generation (~46% of reference size, 1,069-line diff). The fix is a small, opt-in PostgREST enrichment (the x-postgrest-typegen-metadata block) consumed by this producer.

Files

  • src/openapi/types.ts — the cross-repo x-postgrest-typegen-metadata contract.
  • src/openapi/index.tsopenApiToGeneratorMetadata(doc) / metadataToGeneratorMetadata(block). Synthesizes OIDs by name; uses real pg_catalog OIDs for base types so the generator's hardcoded OID checks (VALID_UNNAMED_FUNCTION_ARG_TYPES) and overload sort match.
  • src/openapi/index.test.ts — unit coverage (object kinds, identity/generated, enums, composites, computed fields, SetofOptions, 1:1).
  • package.json./openapi subpath export.
  • knip.json — treat src/**/*.test.ts as entries; ignore experiments/**.
  • experiments/openapi-spike/ — the original spike (REPORT.md) + parity.ts, a dev harness that proves parity (not built/CI-wired).

Verification (byte-identical parity)

Built PostgREST from the companion branch (openapi-metadata on) and compared, for the rich introspection fixture, both producers through the same generator:

reference: live DB -> introspect()                          -> generateTypescript()
candidate: PostgREST OpenAPI -> openApiToGeneratorMetadata() -> generateTypescript()

Result: diff: (identical), 35,637 B == 35,637 B (modulo partition children, which PostgREST intentionally doesn't expose — the harness restricts the reference to the API-reachable relation set).

bun run check-types, bun run format-and-lint, and the adapter unit tests all pass.

Reviewer notes

  • Requires a companion PostgREST change (separate repo/PR): an opt-in openapi-metadata config that emits the x-postgrest-typegen-metadata extension. Without it, openApiToGeneratorMetadata throws a clear error.
  • Stacked on feat(postgrest-typegen): add package with multi-language code generation #302 (base = claude/funny-tesla-wofjdj); rebased onto its latest (incl. sortGeneratorMetadata + the per-table relationships/primary_keys drop), and parity re-verified green.

Blocked by:

supabase/postgrest#6

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f5aef94

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@supabase/postgrest-typegen Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…e generation

Add `@supabase/postgrest-typegen/openapi`: `openApiToGeneratorMetadata(doc)`
maps a PostgREST OpenAPI document's `x-postgrest-typegen-metadata` vendor
extension to `GeneratorMetadata`, so language types can be generated from a
PostgREST URL alone (no database connection) — byte-identical to the live-DB
`introspect()` path for the PostgREST-reachable schema surface.

- `src/openapi/types.ts`: the cross-repo `x-postgrest-typegen-metadata` contract
- `src/openapi/index.ts`: the producer (synthesizes OIDs by name; real pg_catalog
  OIDs for base types so the generator's hardcoded OID checks/overload sort match)
- `src/openapi/index.test.ts`: unit coverage (object kinds, identity/generated,
  enums, composites, computed fields, SetofOptions, 1:1)
- `./openapi` subpath export
- `experiments/openapi-spike/`: the original feasibility spike (REPORT.md) plus
  `parity.ts`, the harness that proved byte-identical parity against a PostgREST
  built with the companion `openapi-metadata` change

Requires the companion PostgREST change (opt-in `openapi-metadata` config that
emits the `x-postgrest-typegen-metadata` extension) — separate repo/PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@avallete
avallete force-pushed the avallete/zen-colden-fa586b branch from 06160d2 to f5aef94 Compare June 20, 2026 14:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06160d20e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Every type referenced by a column/arg/return needs an entry so the
// generator's `typesById.get(type_id)` resolves and `pgTypeToTsType` can run.
const types: PostgresType[] = [];
const typeByName = new Map<string, PostgresType>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Key synthesized types by schema

When the metadata exposes multiple schemas that contain the same enum/composite/table-row type name (for example public.status and private.status), this name-only registry collapses them into one PostgresType. The second ensureType call returns the first type's id/schema, so that schema loses its own Enums/CompositeTypes entry and function args/returns can point at the wrong row type. GeneratorMetadata.types is schema-aware and the generators prefer a type from the current schema, so user-defined types need schema-qualified keys instead of just name.

Useful? React with 👍 / 👎.

Comment on lines +317 to +319
return_type_relation_id: fn.return.relation
? relationId(fn.return.relation.schema, fn.return.relation.name)
: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve composite return relation ids

For functions returning a standalone composite type, the SQL introspection path sets return_type_relation_id from rt.typrelid, which is what lets generateTypescript emit SetofOptions for selecting fields from that composite result. Here the OpenAPI path only sets it when fn.return.relation is present, but the new contract describes that field as table/view-row only, so SETOF my_composite metadata ends up with return_type_relation_id: null and loses the generated SetofOptions despite the composite type having a synthesized type_relation_id above.

Useful? React with 👍 / 👎.

{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["test/global-setup.ts", "test/**/*.test.ts"],
"entry": ["test/global-setup.ts", "test/**/*.test.ts", "src/**/*.test.ts"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Run the new OpenAPI tests in CI

This adds src/**/*.test.ts only as a Knip entry point, but it does not make the test runner execute those tests: I checked .github/workflows/tests.yml, where the postgrest-typegen job runs bun run test, and the package script still invokes scripts/run-tests.ts test/. Because the new regression file lives at src/openapi/index.test.ts, the OpenAPI producer tests are skipped in normal CI unless the test command is extended or the file is moved under test/.

Useful? React with 👍 / 👎.

? relationId(fn.return.relation.schema, fn.return.relation.name)
: null,
is_set_returning_function: fn.return.is_set,
prorows: fn.return.rows ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Default set-returning functions to multi-row

When fn.return.is_set is true but the extension omits rows or sends null, this maps prorows to null, which generateTypescript interprets as not multiple rows. A normal SETOF users RPC without an explicit row estimate therefore generates a single-row Returns type and SetofOptions.isOneToOne: true; the SQL introspection path gets PostgreSQL's default prorows estimate for set-returning functions, so it emits an array return and non-one-to-one options.

Useful? React with 👍 / 👎.

id,
schema: t.schema,
name: t.name,
is_updatable: t.updatable,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require insertable views before emitting Insert

For views, GeneratorMetadata.is_updatable matches the SQL introspection query's (pg_relation_is_updatable(...) & 20) = 20, i.e. the view must support both INSERT and UPDATE before the TypeScript generator emits Insert/Update blocks. Mapping it from t.updatable alone means a view that is update-only in the PostgREST metadata (updatable: true, insertable: false) will still generate an Insert type, exposing writes that the API cannot perform.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant