feat(postgrest-typegen): add OpenAPI producer for credential-free type generation#303
feat(postgrest-typegen): add OpenAPI producer for credential-free type generation#303avallete wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: f5aef94 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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>
06160d2 to
f5aef94
Compare
There was a problem hiding this comment.
💡 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>(); |
There was a problem hiding this comment.
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 👍 / 👎.
| return_type_relation_id: fn.return.relation | ||
| ? relationId(fn.return.relation.schema, fn.return.relation.name) | ||
| : null, |
There was a problem hiding this comment.
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"], |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 👍 / 👎.
What
Adds
@supabase/postgrest-typegen/openapi— a secondGeneratorMetadataproducer alongsideintrospect(db):It maps a PostgREST OpenAPI document's
x-postgrest-typegen-metadatavendor extension toGeneratorMetadata, 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 (thex-postgrest-typegen-metadatablock) consumed by this producer.Files
src/openapi/types.ts— the cross-repox-postgrest-typegen-metadatacontract.src/openapi/index.ts—openApiToGeneratorMetadata(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—./openapisubpath export.knip.json— treatsrc/**/*.test.tsas entries; ignoreexperiments/**.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-metadataon) and compared, for the rich introspection fixture, both producers through the same generator: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
openapi-metadataconfig that emits thex-postgrest-typegen-metadataextension. Without it,openApiToGeneratorMetadatathrows a clear error.claude/funny-tesla-wofjdj); rebased onto its latest (incl.sortGeneratorMetadata+ the per-tablerelationships/primary_keysdrop), and parity re-verified green.Blocked by:
supabase/postgrest#6
🤖 Generated with Claude Code