@supabase/postgrest-typegen 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,28 @@
1
1
  # @supabase/postgrest-typegen
2
2
 
3
3
  Type generation for [PostgREST](https://postgrest.org) from a PostgreSQL
4
- schema. This is the type-generation engine behind `supabase gen types`,
5
- extracted from [postgres-meta](https://github.com/supabase/postgres-meta) into a
6
- small, driver-agnostic library.
4
+ schema: the type-generation engine behind `supabase gen types`, as a small,
5
+ driver-agnostic library.
7
6
 
8
- > **Status:** alpha. The public API is settling as generators and introspection
9
- > are ported.
7
+ > **Status:** alpha. The public API may still change between minor versions.
10
8
 
11
- ## Scope: introspection is permanent, four generators are transitional
9
+ ## Scope: introspection is permanent, three generators are transitional
12
10
 
13
11
  This package's permanent job is introspection and the `GeneratorMetadata`
14
12
  contract (see "Out-of-process generators" below): any *new* language's
15
13
  generator lives in that language's own SDK repo, consuming this package's
16
14
  JSON output, not inside `postgrest-typegen`.
17
15
 
18
- The TypeScript, Go, Python, and Swift generators bundled here are a
16
+ The TypeScript, Go, and Python generators bundled here are a
19
17
  **deliberate transition**, not the target architecture for new languages.
20
- They were ported byte-for-byte from postgres-meta's own templates so
21
- `supabase gen types` kept working unchanged; postgres-meta has since deleted
22
- its copies and consumes this package instead. It's decided that all four
23
- eventually move out to their own SDK repos too (tracked in SDK-1641), not yet
24
- scheduled — sequenced after postgres-meta's cutover to this package settles.
18
+ They move out one language at a time, TypeScript first into supabase-js, then
19
+ Go and Python. Swift's generator is the `supabase-typegen` executable in
20
+ [supabase-swift](https://github.com/supabase/supabase-swift), which
21
+ `supabase gen types --lang swift` runs.
25
22
 
26
23
  > [!IMPORTANT]
27
- > **We are no longer accepting contributions that fix the bundled generators.**
28
- > Because each of the four is moving out to its own language's SDK repository,
24
+ > **We do not accept contributions that fix the bundled generators.**
25
+ > Because each of the three is moving out to its own language's SDK repository,
29
26
  > a fix landed here would have to be made a second time in the destination
30
27
  > repository, and postgres-meta and the CLI only pick it up once they bump
31
28
  > their pinned version of this package. Please open an issue describing the
@@ -57,7 +54,7 @@ generators.
57
54
 
58
55
  ### Stable ordering (`sortGeneratorMetadata`)
59
56
 
60
- The Go/Python/Swift generators emit tables, views, and materialized views in
57
+ The Go/Python generators emit tables, views, and materialized views in
61
58
  `GeneratorMetadata` order, so their output depends on how the producer ordered
62
59
  its collections (a SQL introspector returns rows in environment-dependent heap
63
60
  order). `sortGeneratorMetadata` is a pure pass that canonically sorts every
@@ -65,6 +62,76 @@ collection; **apply it after introspection and before any `generate*` call** so
65
62
  output is deterministic regardless of the producer. Generators expect
66
63
  pre-sorted input and do not re-sort it themselves.
67
64
 
65
+ ### RPC functions (`rpcFunctions`, `resolveRpcFunctions`)
66
+
67
+ `functions` is `pg_proc` as introspected. Which of those functions PostgREST
68
+ exposes under `/rpc`, how overloads group, what a call returns and whether a
69
+ function doubles as a computed field or an embeddable relation are PostgREST
70
+ rules, so `introspect()` applies them once and attaches the result as
71
+ `rpcFunctions`. A generator in any language reads that collection instead of
72
+ reimplementing the rules:
73
+
74
+ ```ts
75
+ for (const fn of metadata.rpcFunctions ?? resolveRpcFunctions(metadata)) {
76
+ // fn.schema, fn.name, and one entry per overload PostgREST can resolve.
77
+ for (const overload of fn.overloads) {
78
+ overload.args; // input parameters: name, mode, type reference, has_default, relation_id
79
+ overload.returns; // type reference, plus relation_id or RETURNS TABLE columns
80
+ overload.is_set_returning; // the response is an array
81
+ overload.setof_options; // embeddable like a relation: from, to, is_one_to_one
82
+ overload.computed_field_of; // selectable as a column of this relation
83
+ overload.error; // why PostgREST cannot resolve a call, else null
84
+ }
85
+ }
86
+ ```
87
+
88
+ A type reference carries `type_id`, `format` (the type's name, spelled like
89
+ `PostgresColumn.format`) and `type_schema`, so a generator maps it with the
90
+ same table it uses for columns. An overload's `error` is the message the
91
+ TypeScript generator emits as the `Returns` type of an unresolvable overload:
92
+ two overloads PostgREST cannot tell apart, or a sole unnamed row parameter,
93
+ which PostgREST never matches under `/rpc` (such a function is a computed
94
+ field or computed relationship, reached by embedding it from its relation).
95
+ Overloads are ordered by argument list, then by return type; the
96
+ TypeScript generator emits them in that order.
97
+
98
+ `resolveRpcFunctions` is pure and deterministic, so a custom producer attaches
99
+ it the same way, and a document without the collection (produced before it
100
+ existed) still generates: the TypeScript generator falls back to running the
101
+ pass itself.
102
+
103
+ ### View column nullability
104
+
105
+ PostgreSQL never marks view columns `NOT NULL`, so `introspect()` works out
106
+ `is_nullable` for view and materialized view columns from the view's query
107
+ tree. A column is not nullable when it is a plain reference to a `NOT NULL`
108
+ column (directly, through another view, a subquery, a non-recursive CTE, a
109
+ join alias or a `GROUP BY` key) and nothing on the way puts it on the nullable
110
+ side of an outer join, in a set operation such as `UNION` or under
111
+ `GROUPING SETS`/`ROLLUP`/`CUBE`. This works the same on every supported
112
+ PostgreSQL version. Expressions stay nullable, even ones that cannot return
113
+ null.
114
+
115
+ For a column the catalog cannot prove non-null, such as `coalesce(x, 0)` or
116
+ `count(*)`, add a line containing only `@notNull` to its comment:
117
+
118
+ ```sql
119
+ comment on column my_view.total is '@notNull';
120
+ ```
121
+
122
+ The directive is honored on view and materialized view columns only, and the
123
+ comment is passed through unchanged.
124
+
125
+ The inference describes the rows a query reads, so a few values can still be
126
+ null where the type says otherwise:
127
+
128
+ - Rows returned by a write through an `INSTEAD OF` trigger are the trigger's
129
+ `NEW`, so a column the trigger leaves unset comes back null.
130
+ - PostgreSQL does not enforce `NOT NULL` inside composite values, so a column
131
+ whose type is a view's row type can hold a row with null fields.
132
+ - A materialized view keeps the rows of its last refresh, including nulls from
133
+ before its source column became `NOT NULL`.
134
+
68
135
  ### Runtime validation (opt-in)
69
136
 
70
137
  `GeneratorMetadata` is backed by an [ArkType](https://arktype.io) schema, so a
@@ -104,10 +171,48 @@ const json = serializeGeneratorMetadata(sortGeneratorMetadata(metadata));
104
171
  generatorMetadataJsonSchema;
105
172
  ```
106
173
 
107
- `GeneratorMetadata.version` is bumped whenever the shape changes in a way a
108
- consumer should branch on, since an out-of-process consumer only sees the
109
- serialized document and can't otherwise detect a shape change until something
110
- breaks at read time.
174
+ ### Contract compatibility (`GeneratorMetadata.version`)
175
+
176
+ `GeneratorMetadata` is read by code that does not install this package: an
177
+ out-of-process generator sees only the serialized document, and postgres-meta
178
+ and the Supabase CLI consume it through a pinned registry version. The shape
179
+ therefore changes under one rule:
180
+
181
+ - **A new field is optional and has a documented fallback.** The fallback
182
+ lives in the consumers that read the field, so a document produced by any
183
+ released version still parses and generates what it generated before. The
184
+ view flags `is_insert_enabled` and `is_update_enabled` are the model: they
185
+ are optional, and the TypeScript generator, the only one that emits view
186
+ write types, falls back to `is_updatable` when they are absent. The
187
+ `rpcFunctions` collection follows the same rule: a generator that does not
188
+ find it runs `resolveRpcFunctions` on the raw `functions` itself.
189
+ - **Anything else moves `GENERATOR_METADATA_VERSION`:** removing or renaming a
190
+ field, making one required, narrowing its type, or changing what an existing
191
+ value means. The version is `1`.
192
+
193
+ `parseGeneratorMetadata` and `generatorMetadataJsonSchema` pin the exact
194
+ version string, so a consumer on another version fails at parse time with a
195
+ clear error instead of misreading the document. An out-of-process generator
196
+ should do the same and exit distinctly when it meets a version it does not
197
+ know; `@supabase/typegen` maps Dart's exit code 65 to `MetadataRejectedError`
198
+ for that reason.
199
+
200
+ The contract version and the package version are independent:
201
+
202
+ - `GENERATOR_METADATA_VERSION` changes only with an incompatible shape change.
203
+ A release that moves it is marked breaking in the changelog, and consumers
204
+ that read the serialized document pin this number, not the npm version.
205
+ - The package follows semantic versioning and is on `0.x`. A minor release
206
+ may change generated output or the TypeScript
207
+ API, with breaking changes called out in the changelog; a patch release
208
+ changes neither on purpose. The four generators' output is part of the
209
+ public surface: any change to it is described in the pull request so it
210
+ reaches the release notes that postgres-meta and the CLI read before bumping.
211
+
212
+ Changes to `src/types.ts` and `src/introspection/` are reviewed by
213
+ [@spydon](https://github.com/spydon), listed in `CODEOWNERS` for this package
214
+ next to the SDK team, so every contract change has one person who checks it
215
+ against this rule.
111
216
 
112
217
  ### Generators
113
218
 
@@ -116,7 +221,6 @@ import {
116
221
  generateTypescript, // async (formats its output)
117
222
  generateGo,
118
223
  generatePython,
119
- generateSwift,
120
224
  } from "@supabase/postgrest-typegen/generation";
121
225
  ```
122
226
 
@@ -125,7 +229,6 @@ import {
125
229
  | `generateTypescript` | `{ detectOneToOneRelationships?, postgrestVersion?, defaultSchema?, format? }` |
126
230
  | `generateGo` | — |
127
231
  | `generatePython` | — |
128
- | `generateSwift` | `{ accessControl?: 'internal' \| 'public' \| 'private' \| 'package' }` |
129
232
 
130
233
  Every generator returns the complete contents of the generated file, ending
131
234
  with a single newline, so consumers write the result as-is.
@@ -138,6 +241,22 @@ worker-pool-backed formatter) and it is never loaded or needed. Calling
138
241
  `generateTypescript` without `format` when `oxfmt` is missing throws an error
139
242
  saying so.
140
243
 
244
+ ## Generating types without database credentials
245
+
246
+ `GeneratorMetadata` is the pluggable contract, so generating types for a
247
+ project without its database password means replacing the introspection
248
+ producer, not touching any generator. No such producer ships in this package.
249
+
250
+ Generating from PostgREST's OpenAPI output needs PostgREST to include the
251
+ introspection details the generators depend on: the standard OpenAPI document
252
+ alone carries about half of what the TypeScript output needs. That is the
253
+ `x-postgrest-typegen-metadata` extension proposed in
254
+ [supabase/postgrest#6](https://github.com/supabase/postgrest/pull/6). A
255
+ producer that reads it and matches `introspect()` byte for byte exists in
256
+ [supabase/pg-toolbelt#303](https://github.com/supabase/pg-toolbelt/pull/303)
257
+ and is the starting point for porting one here once the extension is
258
+ available.
259
+
141
260
  ## Installation
142
261
 
143
262
  Not yet published; consumed in-repo for now (`packages/postgrest-typegen`).
@@ -149,6 +268,20 @@ npm install pg
149
268
  npm install oxfmt@0.66.0
150
269
  ```
151
270
 
271
+ ## postgrest-js type tests
272
+
273
+ Every pull request that touches this package also runs postgrest-js's type tests against the TypeScript this pull request generates. The `postgrest-js type tests` job in `postgrest-typegen-validate.yml` checks out supabase-js, starts the database fixture in `packages/core/postgrest-js/test/supabase`, writes `test/types.generated.ts` with `bun run generate:postgrest-js-types`, and runs `test:types:ci` over every TypeScript version postgrest-js supports. The assertions are the hand-written `*.test-d.ts` files in supabase-js, so the generated file is allowed to change as long as they keep passing.
274
+
275
+ To run it locally, start that fixture with `supabase --workdir packages/core/postgrest-js/test db start` in a supabase-js checkout, then:
276
+
277
+ ```bash
278
+ bun run generate:postgrest-js-types postgresql://postgres:postgres@127.0.0.1:54322/postgres <supabase-js>/packages/core/postgrest-js/test/types.generated.ts
279
+ ```
280
+
281
+ Then run `node scripts/update-json-type.js`, `pnpm exec prettier --write test/types.generated.ts` and `pnpm run test:types:ci` in `packages/core/postgrest-js`.
282
+
283
+ This job covers TypeScript only. Findings in the Go and Python generators stay with their own unit tests here, and move with those generators to their SDK repositories.
284
+
152
285
  ## Releasing
153
286
 
154
287
  Unlike the rest of this repo, merging a change here does not open a release pull request on its own. Releases are cut on demand: run the `Release postgrest-typegen` workflow from the Actions tab, which runs release-please against `release-please-config.postgrest-typegen.json` and opens (or updates) the release pull request for this package. Merging that pull request tags the release and publishes to npm. Once npm serves the new version, the same workflow opens a pull request that bumps the exact pin in `packages/typegen`.
@@ -2,4 +2,3 @@ export { sortGeneratorMetadata } from "../sort.ts";
2
2
  export { generateGo } from "./go.ts";
3
3
  export { generatePython } from "./python.ts";
4
4
  export { generateTypescript, type GenerateTypescriptOptions, pgTypeToTsType, type TypeResolutionContext, } from "./typescript.ts";
5
- export { type AccessControl, generateSwift, type GenerateSwiftOptions, } from "./swift.ts";
@@ -2,4 +2,3 @@ export { sortGeneratorMetadata } from "../sort.js";
2
2
  export { generateGo } from "./go.js";
3
3
  export { generatePython } from "./python.js";
4
4
  export { generateTypescript, pgTypeToTsType, } from "./typescript.js";
5
- export { generateSwift, } from "./swift.js";
@@ -13,7 +13,8 @@ export interface GenerateTypescriptOptions {
13
13
  /**
14
14
  * Schema treated as the default for the generated `Tables`/`Enums`/etc.
15
15
  * helper types. Replaces postgres-meta's `GENERATE_TYPES_DEFAULT_SCHEMA`
16
- * env read. Default `'public'`.
16
+ * env read. Default `'public'`. When it is not among the generated schemas,
17
+ * the first generated schema is used instead, so the helpers keep resolving.
17
18
  */
18
19
  defaultSchema?: string;
19
20
  /**