@supabase/postgrest-typegen 0.2.4 → 0.3.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
@@ -113,19 +113,27 @@ breaks at read time.
113
113
 
114
114
  ```ts
115
115
  import {
116
- generateTypescript, // async (uses prettier)
116
+ generateTypescript, // async (formats its output)
117
117
  generateGo,
118
118
  generatePython,
119
119
  generateSwift,
120
120
  } from "@supabase/postgrest-typegen/generation";
121
121
  ```
122
122
 
123
- | Function | Options |
124
- | -------------------- | ----------------------------------------------------------------------- |
125
- | `generateTypescript` | `{ detectOneToOneRelationships?, postgrestVersion?, defaultSchema? }` |
126
- | `generateGo` | — |
127
- | `generatePython` | — |
128
- | `generateSwift` | `{ accessControl?: 'internal' \| 'public' \| 'private' \| 'package' }` |
123
+ | Function | Options |
124
+ | -------------------- | ------------------------------------------------------------------------------ |
125
+ | `generateTypescript` | `{ detectOneToOneRelationships?, postgrestVersion?, defaultSchema?, format? }` |
126
+ | `generateGo` | — |
127
+ | `generatePython` | — |
128
+ | `generateSwift` | `{ accessControl?: 'internal' \| 'public' \| 'private' \| 'package' }` |
129
+
130
+ `generateTypescript` formats its output with `oxfmt` unless `format` is
131
+ supplied. `oxfmt` is an optional peer dependency, imported only when that
132
+ default runs: install it to use the default formatter, or pass your own
133
+ `format` (an identity function, for byte-identical unformatted output, or a
134
+ worker-pool-backed formatter) and it is never loaded or needed. Calling
135
+ `generateTypescript` without `format` when `oxfmt` is missing throws an error
136
+ saying so.
129
137
 
130
138
  ## Installation
131
139
 
@@ -134,6 +142,8 @@ Not yet published; consumed in-repo for now (`packages/postgrest-typegen`).
134
142
  ```bash
135
143
  # pg is a peer of your application, not bundled here
136
144
  npm install pg
145
+ # only if you rely on generateTypescript's default formatter
146
+ npm install oxfmt@0.66.0
137
147
  ```
138
148
 
139
149
  ## Releasing
@@ -4,8 +4,7 @@
4
4
  */
5
5
  export const generateGo = ({ schemas, tables, views, materializedViews, columns, types, }) => {
6
6
  const columnsByTableId = columns.reduce((acc, curr) => {
7
- acc[curr.table_id] ??= [];
8
- acc[curr.table_id].push(curr);
7
+ (acc[curr.table_id] ??= []).push(curr);
9
8
  return acc;
10
9
  }, {});
11
10
  const compositeTypes = types.filter((type) => type.attributes.length > 0);
@@ -50,7 +49,7 @@ function formatForGoTypeName(name) {
50
49
  .split(/[^a-zA-Z0-9]/)
51
50
  .map((word) => {
52
51
  if (word) {
53
- return `${word[0].toUpperCase()}${word.slice(1)}`;
52
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
54
53
  }
55
54
  else {
56
55
  return "";
@@ -55,8 +55,7 @@ class PythonContext {
55
55
  this.schemas = Object.fromEntries(schemas.map((schema) => [schema.name, schema]));
56
56
  this.types = Object.fromEntries(types.map((type) => [qualifiedTypeName(type.schema, type.name), type]));
57
57
  this.columns = columns.reduce((acc, curr) => {
58
- acc[curr.table_id] ??= [];
59
- acc[curr.table_id].push(curr);
58
+ (acc[curr.table_id] ??= []).push(curr);
60
59
  return acc;
61
60
  }, {});
62
61
  this.user_enums = Object.fromEntries(types
@@ -66,16 +65,25 @@ class PythonContext {
66
65
  new PythonEnum(type),
67
66
  ]));
68
67
  }
68
+ getSchema(name) {
69
+ const schema = this.schemas[name];
70
+ if (!schema) {
71
+ throw new Error(`Schema ${JSON.stringify(name)} is missing from the generator metadata`);
72
+ }
73
+ return schema;
74
+ }
69
75
  resolveTypeName(name, typeSchema) {
70
76
  const qualified = qualifiedTypeName(typeSchema, name);
71
- if (qualified in this.user_enums) {
72
- return this.user_enums[qualified].name;
77
+ const userEnum = this.user_enums[qualified];
78
+ if (userEnum) {
79
+ return userEnum.name;
73
80
  }
74
- if (name in PY_TYPE_MAP) {
75
- return PY_TYPE_MAP[name];
81
+ const mappedType = PY_TYPE_MAP[name];
82
+ if (mappedType !== undefined) {
83
+ return mappedType;
76
84
  }
77
- if (qualified in this.types) {
78
- const type = this.types[qualified];
85
+ const type = this.types[qualified];
86
+ if (type) {
79
87
  return `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}`;
80
88
  }
81
89
  return "Any";
@@ -108,7 +116,7 @@ class PythonContext {
108
116
  // Postgres, so every field is inherently nullable.
109
117
  return new PythonBaseModelAttr(attribute.name, type, true);
110
118
  });
111
- const schema = this.schemas[type.schema];
119
+ const schema = this.getSchema(type.schema);
112
120
  return new PythonBaseModel(type.name, schema, attributeEntries);
113
121
  }
114
122
  columnsToClassAttrs(table_id) {
@@ -129,7 +137,7 @@ class PythonContext {
129
137
  });
130
138
  }
131
139
  tableToClass(table) {
132
- const schema = this.schemas[table.schema];
140
+ const schema = this.getSchema(table.schema);
133
141
  const select = new PythonBaseModel(table.name, schema, this.columnsToClassAttrs(table.id));
134
142
  const insert = new PythonTypedDict(table.name, "Insert", schema, this.columnsToDictAttrs(table.id, false));
135
143
  const update = new PythonTypedDict(table.name, "Update", schema, this.columnsToDictAttrs(table.id, true));
@@ -137,11 +145,11 @@ class PythonContext {
137
145
  }
138
146
  viewToClass(view) {
139
147
  const attributes = this.columnsToClassAttrs(view.id);
140
- return new PythonBaseModel(view.name, this.schemas[view.schema], attributes);
148
+ return new PythonBaseModel(view.name, this.getSchema(view.schema), attributes);
141
149
  }
142
150
  matViewToClass(matview) {
143
151
  const attributes = this.columnsToClassAttrs(matview.id);
144
- return new PythonBaseModel(matview.name, this.schemas[matview.schema], attributes);
152
+ return new PythonBaseModel(matview.name, this.getSchema(matview.schema), attributes);
145
153
  }
146
154
  }
147
155
  const qualifiedTypeName = (schema, name) => `${schema}.${name}`;
@@ -332,7 +340,7 @@ function formatForPyClassName(name) {
332
340
  .split(/[^a-zA-Z0-9]/)
333
341
  .map((word) => {
334
342
  if (word) {
335
- return `${word[0].toUpperCase()}${word.slice(1)}`;
343
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
336
344
  }
337
345
  else {
338
346
  return "";
@@ -177,9 +177,9 @@ export const generateSwift = (metadata, opts = {}) => {
177
177
  t.id,
178
178
  [],
179
179
  ]));
180
- columns
181
- .filter((c) => c.table_id in columnsByTableId)
182
- .forEach((c) => columnsByTableId[c.table_id].push(c));
180
+ for (const column of columns) {
181
+ columnsByTableId[column.table_id]?.push(column);
182
+ }
183
183
  let output = [
184
184
  "import Foundation",
185
185
  "import Supabase",
@@ -322,7 +322,7 @@ function formatForSwiftTypeName(name) {
322
322
  .split(/[^a-zA-Z0-9]+/)
323
323
  .map((word) => {
324
324
  if (word) {
325
- return `${word[0].toUpperCase()}${word.slice(1)}`;
325
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
326
326
  }
327
327
  else {
328
328
  return "";
@@ -17,9 +17,11 @@ export interface GenerateTypescriptOptions {
17
17
  */
18
18
  defaultSchema?: string;
19
19
  /**
20
- * Formatter used on the generated output. Defaults to `oxfmt`, called
21
- * inline. Callers on a latency-sensitive request path (e.g. postgres-meta's
22
- * hosted `/generators/typescript` route) can substitute a worker-pool-backed
20
+ * Formatter used on the generated output. Defaults to `oxfmt`, an optional
21
+ * peer dependency that is imported only when this default runs, so callers
22
+ * that always pass their own formatter need not install it. Callers on a
23
+ * latency-sensitive request path (e.g. postgres-meta's hosted
24
+ * `/generators/typescript` route) can substitute a worker-pool-backed
23
25
  * formatter here instead of blocking the event loop on every call.
24
26
  */
25
27
  format?: (code: string) => Promise<string>;
@@ -1,13 +1,16 @@
1
- // oxlint-disable typescript/restrict-template-expressions -- this is a verbatim port of
2
- // postgres-meta's template, which intentionally interpolates arrays/values into template
3
- // literals (relying on Array#toString) to build the output. Preserving that is required for
4
- // byte-parity, and the offending expressions sit inside template literals where a targeted
5
- // disable comment would corrupt the emitted string.
6
- import { format as oxfmtFormat } from "oxfmt";
7
1
  import { VALID_FUNCTION_ARGS_MODE, VALID_UNNAMED_FUNCTION_ARG_TYPES, } from "./constants.js";
8
2
  import { compareStrings } from "../collation.js";
3
+ const loadOxfmt = async () => {
4
+ try {
5
+ return await import("oxfmt");
6
+ }
7
+ catch (error) {
8
+ throw new Error("oxfmt is not installed. It is an optional peer dependency that only the default formatter needs: install it, or pass a `format` option to generateTypescript.", { cause: error });
9
+ }
10
+ };
9
11
  const defaultFormat = async (code) => {
10
- const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
12
+ const { format } = await loadOxfmt();
13
+ const { code: formatted, errors } = await format("output.ts", code, {
11
14
  semi: false,
12
15
  printWidth: 80,
13
16
  });
@@ -59,23 +62,33 @@ export const generateTypescript = async (metadata, opts = {}) => {
59
62
  tablesNamesByTableId[tableLike.id] = tableLike.name;
60
63
  }
61
64
  for (const column of columns) {
62
- if (column.table_id in columnsByTableId) {
63
- columnsByTableId[column.table_id].push(column);
64
- }
65
+ columnsByTableId[column.table_id]?.push(column);
65
66
  }
67
+ const getColumns = (tableId) => {
68
+ const tableColumns = columnsByTableId[tableId];
69
+ if (!tableColumns) {
70
+ throw new Error(`Relation ${tableId} is missing from the generator metadata`);
71
+ }
72
+ return tableColumns;
73
+ };
74
+ const getSchemaIntrospection = (schemaName) => {
75
+ const schemaIntrospection = introspectionBySchema[schemaName];
76
+ if (!schemaIntrospection) {
77
+ throw new Error(`Schema ${JSON.stringify(schemaName)} is missing from the generator metadata`);
78
+ }
79
+ return schemaIntrospection;
80
+ };
66
81
  for (const type of types) {
67
82
  typesById.set(type.id, type);
68
83
  // Save all the types that are relation types for quicker lookup
69
84
  if (type.type_relation_id) {
70
85
  relationTypeByIds.set(type.id, type);
71
86
  }
72
- if (type.schema in introspectionBySchema) {
73
- if (type.enums.length > 0) {
74
- introspectionBySchema[type.schema].enums.push(type);
75
- }
76
- if (type.attributes.length > 0) {
77
- introspectionBySchema[type.schema].compositeTypes.push(type);
78
- }
87
+ if (type.enums.length > 0) {
88
+ introspectionBySchema[type.schema]?.enums.push(type);
89
+ }
90
+ if (type.attributes.length > 0) {
91
+ introspectionBySchema[type.schema]?.compositeTypes.push(type);
79
92
  }
80
93
  }
81
94
  const relationshipsByRelation = new Map();
@@ -105,56 +118,51 @@ export const generateTypescript = async (metadata, opts = {}) => {
105
118
  }`;
106
119
  }
107
120
  for (const table of tables) {
108
- if (table.schema in introspectionBySchema) {
109
- introspectionBySchema[table.schema].tables.push({
110
- table,
111
- relationships: getRelationships(table),
112
- });
113
- }
121
+ introspectionBySchema[table.schema]?.tables.push({
122
+ table,
123
+ relationships: getRelationships(table),
124
+ });
114
125
  }
115
126
  for (const table of foreignTables) {
116
- if (table.schema in introspectionBySchema) {
117
- introspectionBySchema[table.schema].tables.push({
118
- table,
119
- relationships: getRelationships(table),
120
- });
121
- }
127
+ introspectionBySchema[table.schema]?.tables.push({
128
+ table,
129
+ relationships: getRelationships(table),
130
+ });
122
131
  }
123
132
  for (const view of views) {
124
- if (view.schema in introspectionBySchema) {
125
- introspectionBySchema[view.schema].views.push({
126
- view,
127
- relationships: getRelationships(view),
128
- });
129
- }
133
+ introspectionBySchema[view.schema]?.views.push({
134
+ view,
135
+ relationships: getRelationships(view),
136
+ });
130
137
  }
131
138
  for (const materializedView of materializedViews) {
132
- if (materializedView.schema in introspectionBySchema) {
133
- introspectionBySchema[materializedView.schema].views.push({
134
- view: {
135
- ...materializedView,
136
- is_updatable: false,
137
- is_insert_enabled: false,
138
- is_update_enabled: false,
139
- },
140
- relationships: getRelationships(materializedView),
141
- });
142
- }
139
+ introspectionBySchema[materializedView.schema]?.views.push({
140
+ view: {
141
+ ...materializedView,
142
+ is_updatable: false,
143
+ is_insert_enabled: false,
144
+ is_update_enabled: false,
145
+ },
146
+ relationships: getRelationships(materializedView),
147
+ });
143
148
  }
144
149
  // Helper function to get table/view name from relation id
145
150
  const getTableNameFromRelationId = (relationId, returnTypeId) => {
146
151
  if (!relationId)
147
152
  return null;
148
- if (tablesNamesByTableId[relationId])
149
- return tablesNamesByTableId[relationId];
153
+ const tableName = tablesNamesByTableId[relationId];
154
+ if (tableName)
155
+ return tableName;
150
156
  // if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type
151
157
  const reltype = returnTypeId ? relationTypeByIds.get(returnTypeId) : null;
152
158
  return reltype ? reltype.name : null;
153
159
  };
154
160
  for (const func of functions) {
155
- if (func.schema in introspectionBySchema) {
161
+ const schemaIntrospection = introspectionBySchema[func.schema];
162
+ if (schemaIntrospection) {
156
163
  // Get all input args (in, inout, variadic modes)
157
164
  const inArgs = func.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode));
165
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
158
166
  if (
159
167
  // Case 1: Function has no parameters
160
168
  inArgs.length === 0 ||
@@ -171,16 +179,16 @@ export const generateTypescript = async (metadata, opts = {}) => {
171
179
  // Case 4: Single unnamed parameter of valid type (json, jsonb, text)
172
180
  // Exclude all functions definitions that have only one single argument unnamed argument that isn't
173
181
  // a json/jsonb/text as it won't be considered by PostgREST
174
- (inArgs.length === 1 &&
175
- inArgs[0].name === "" &&
176
- (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) ||
182
+ (soleInArg &&
183
+ soleInArg.name === "" &&
184
+ (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(soleInArg.type_id) ||
177
185
  // OR if the function have a single unnamed args which is another table (embeded function)
178
- (relationTypeByIds.get(inArgs[0].type_id) &&
186
+ (relationTypeByIds.get(soleInArg.type_id) &&
179
187
  getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)) ||
180
188
  // OR if the function takes a table row but doesn't qualify as embedded (for error reporting)
181
- (relationTypeByIds.get(inArgs[0].type_id) &&
189
+ (relationTypeByIds.get(soleInArg.type_id) &&
182
190
  !getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id))))) {
183
- introspectionBySchema[func.schema].functions.push({ fn: func, inArgs });
191
+ schemaIntrospection.functions.push({ fn: func, inArgs });
184
192
  }
185
193
  }
186
194
  }
@@ -188,12 +196,12 @@ export const generateTypescript = async (metadata, opts = {}) => {
188
196
  // combines tables + foreign tables, `.views` combines views + materialized
189
197
  // views — so the cross-collection name sort can't be expressed by the
190
198
  // single-collection `sortGeneratorMetadata` pass and stays here.
191
- for (const schema in introspectionBySchema) {
192
- introspectionBySchema[schema].tables.sort((a, b) => compareStrings(a.table.name, b.table.name));
193
- introspectionBySchema[schema].views.sort((a, b) => compareStrings(a.view.name, b.view.name));
194
- introspectionBySchema[schema].functions.sort((a, b) => compareStrings(a.fn.name, b.fn.name));
195
- introspectionBySchema[schema].enums.sort((a, b) => compareStrings(a.name, b.name));
196
- introspectionBySchema[schema].compositeTypes.sort((a, b) => compareStrings(a.name, b.name));
199
+ for (const schemaIntrospection of Object.values(introspectionBySchema)) {
200
+ schemaIntrospection.tables.sort((a, b) => compareStrings(a.table.name, b.table.name));
201
+ schemaIntrospection.views.sort((a, b) => compareStrings(a.view.name, b.view.name));
202
+ schemaIntrospection.functions.sort((a, b) => compareStrings(a.fn.name, b.fn.name));
203
+ schemaIntrospection.enums.sort((a, b) => compareStrings(a.name, b.name));
204
+ schemaIntrospection.compositeTypes.sort((a, b) => compareStrings(a.name, b.name));
197
205
  }
198
206
  const getFunctionTsReturnType = (fn, returnType) => {
199
207
  // Determine if this function should have SetofOptions
@@ -213,8 +221,9 @@ export const generateTypescript = async (metadata, opts = {}) => {
213
221
  }
214
222
  // Case 2: if the function has a single table argument, we need to add SetofOptions to allow selecting sub fields of the table
215
223
  // and set the right "from" and "to" values to allow selecting from a table row
216
- if (fn.args.length === 1) {
217
- const relationType = relationTypeByIds.get(fn.args[0].type_id);
224
+ const soleArg = fn.args.length === 1 ? fn.args[0] : undefined;
225
+ if (soleArg) {
226
+ const relationType = relationTypeByIds.get(soleArg.type_id);
218
227
  // Only add SetofOptions for functions with table arguments (embedded functions)
219
228
  // or specific functions that RETURNS table-name
220
229
  if (relationType) {
@@ -268,7 +277,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
268
277
  introspectionBySchema[schema.name]?.views.find(({ view: { id } }) => id === fn.return_type_relation_id)?.view;
269
278
  if (relation) {
270
279
  return `{
271
- ${columnsByTableId[relation.id]
280
+ ${getColumns(relation.id)
272
281
  .map((column) => generateColumnTsDefinition(schema, {
273
282
  name: column.name,
274
283
  format: column.format,
@@ -288,9 +297,10 @@ export const generateTypescript = async (metadata, opts = {}) => {
288
297
  };
289
298
  // Special error case for functions that take table row but don't qualify as embedded functions
290
299
  const hasTableRowError = (fn, inArgs) => {
291
- if (inArgs.length === 1 &&
292
- inArgs[0].name === "" &&
293
- relationTypeByIds.get(inArgs[0].type_id) &&
300
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
301
+ if (soleInArg &&
302
+ soleInArg.name === "" &&
303
+ relationTypeByIds.get(soleInArg.type_id) &&
294
304
  !getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id)) {
295
305
  return true;
296
306
  }
@@ -301,30 +311,33 @@ export const generateTypescript = async (metadata, opts = {}) => {
301
311
  // If there is a single function definition, there is no conflict
302
312
  if (fns.length <= 1)
303
313
  return null;
314
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
304
315
  // Generic conflict detection patterns
305
316
  // Pattern 1: No-args vs default-args conflicts
306
317
  if (inArgs.length === 0) {
307
318
  const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => {
308
319
  if (otherFn === fn)
309
320
  return false;
310
- return (otherInArgs.length === 1 &&
311
- otherInArgs[0].name === "" &&
312
- otherInArgs[0].has_default);
321
+ const otherSoleInArg = otherInArgs.length === 1 ? otherInArgs[0] : undefined;
322
+ return (otherSoleInArg &&
323
+ otherSoleInArg.name === "" &&
324
+ otherSoleInArg.has_default);
313
325
  });
314
- if (conflictingFns.length > 0) {
315
- const conflictingFn = conflictingFns[0];
326
+ const conflictingFn = conflictingFns[0];
327
+ if (conflictingFn) {
316
328
  const returnTypeName = typesById.get(conflictingFn.fn.return_type_id)?.name || "unknown";
317
329
  return `Could not choose the best candidate function between: ${schema.name}.${fn.name}(), ${schema.name}.${fn.name}( => ${returnTypeName}). Try renaming the parameters or the function itself in the database so function overloading can be resolved`;
318
330
  }
319
331
  }
320
332
  // Pattern 2: Same parameter name but different types (unresolvable overloads)
321
- if (inArgs.length === 1 && inArgs[0].name !== "") {
333
+ if (soleInArg && soleInArg.name !== "") {
322
334
  const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => {
323
335
  if (otherFn === fn)
324
336
  return false;
325
- return (otherInArgs.length === 1 &&
326
- otherInArgs[0].name === inArgs[0].name &&
327
- otherInArgs[0].type_id !== inArgs[0].type_id);
337
+ const otherSoleInArg = otherInArgs.length === 1 ? otherInArgs[0] : undefined;
338
+ return (otherSoleInArg &&
339
+ otherSoleInArg.name === soleInArg.name &&
340
+ otherSoleInArg.type_id !== soleInArg.type_id);
328
341
  });
329
342
  if (conflictingFns.length > 0) {
330
343
  const allConflictingFunctions = [{ fn, inArgs }, ...conflictingFns];
@@ -443,11 +456,12 @@ export const generateTypescript = async (metadata, opts = {}) => {
443
456
  * See supabase/postgres-meta#1034.
444
457
  */
445
458
  const isComputedFieldOf = (fn, inArgs, relation) => {
446
- if (inArgs.length === 1) {
459
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
460
+ if (soleInArg) {
447
461
  // `relationTypeByIds` is already "types backed by a relation", so a hit
448
462
  // identifies the relation regardless of how the argument was spelled.
449
463
  // A `variadic` argument is an array type and never appears here.
450
- const argType = relationTypeByIds.get(inArgs[0].type_id);
464
+ const argType = relationTypeByIds.get(soleInArg.type_id);
451
465
  if (argType) {
452
466
  return argType.type_relation_id === relation.id;
453
467
  }
@@ -463,7 +477,7 @@ export type Json = string | number | boolean | null | { [key: string]: Json | un
463
477
  export type Database = {
464
478
  ${internal_supabase_schema}
465
479
  ${schemas.map((schema) => {
466
- const { tables: schemaTables, views: schemaViews, functions: schemaFunctions, enums: schemaEnums, compositeTypes: schemaCompositeTypes, } = introspectionBySchema[schema.name];
480
+ const { tables: schemaTables, views: schemaViews, functions: schemaFunctions, enums: schemaEnums, compositeTypes: schemaCompositeTypes, } = getSchemaIntrospection(schema.name);
467
481
  return `${JSON.stringify(schema.name)}: {
468
482
  Tables: {
469
483
  ${schemaTables.length === 0
@@ -471,7 +485,7 @@ export type Database = {
471
485
  : schemaTables.map(({ table, relationships, }) => `${JSON.stringify(table.name)}: {
472
486
  Row: {
473
487
  ${[
474
- ...columnsByTableId[table.id].map((column) => generateColumnTsDefinition(schema, {
488
+ ...getColumns(table.id).map((column) => generateColumnTsDefinition(schema, {
475
489
  name: column.name,
476
490
  format: column.format,
477
491
  type_schema: column.type_schema,
@@ -486,7 +500,7 @@ export type Database = {
486
500
  ]}
487
501
  }
488
502
  Insert: {
489
- ${columnsByTableId[table.id].map((column) => {
503
+ ${getColumns(table.id).map((column) => {
490
504
  if (column.identity_generation === "ALWAYS" ||
491
505
  column.is_generated) {
492
506
  return `${JSON.stringify(column.name)}?: never`;
@@ -503,7 +517,7 @@ export type Database = {
503
517
  })}
504
518
  }
505
519
  Update: {
506
- ${columnsByTableId[table.id].map((column) => {
520
+ ${getColumns(table.id).map((column) => {
507
521
  if (column.identity_generation === "ALWAYS" ||
508
522
  column.is_generated) {
509
523
  return `${JSON.stringify(column.name)}?: never`;
@@ -528,7 +542,7 @@ export type Database = {
528
542
  : schemaViews.map(({ view, relationships, }) => `${JSON.stringify(view.name)}: {
529
543
  Row: {
530
544
  ${[
531
- ...columnsByTableId[view.id].map((column) => generateColumnTsDefinition(schema, {
545
+ ...getColumns(view.id).map((column) => generateColumnTsDefinition(schema, {
532
546
  name: column.name,
533
547
  format: column.format,
534
548
  type_schema: column.type_schema,
@@ -545,7 +559,7 @@ export type Database = {
545
559
  // version 1 documents) falls back to the old gate.
546
560
  (view.is_insert_enabled ?? view.is_updatable)
547
561
  ? `Insert: {
548
- ${columnsByTableId[view.id].map((column) => {
562
+ ${getColumns(view.id).map((column) => {
549
563
  if (!column.is_updatable) {
550
564
  return `${JSON.stringify(column.name)}?: never`;
551
565
  }
@@ -561,7 +575,7 @@ export type Database = {
561
575
  `
562
576
  : ""}${(view.is_update_enabled ?? view.is_updatable)
563
577
  ? `Update: {
564
- ${columnsByTableId[view.id].map((column) => {
578
+ ${getColumns(view.id).map((column) => {
565
579
  if (!column.is_updatable) {
566
580
  return `${JSON.stringify(column.name)}?: never`;
567
581
  }
@@ -586,12 +600,11 @@ export type Database = {
586
600
  return "[_ in never]: never";
587
601
  }
588
602
  const schemaFunctionsGroupedByName = schemaFunctions.reduce((acc, curr) => {
589
- acc[curr.fn.name] ??= [];
590
- acc[curr.fn.name].push(curr);
603
+ (acc[curr.fn.name] ??= []).push(curr);
591
604
  return acc;
592
605
  }, {});
593
- for (const fnName in schemaFunctionsGroupedByName) {
594
- schemaFunctionsGroupedByName[fnName].sort((a, b) => compareStrings(a.fn.argument_types, b.fn.argument_types) ||
606
+ for (const fns of Object.values(schemaFunctionsGroupedByName)) {
607
+ fns.sort((a, b) => compareStrings(a.fn.argument_types, b.fn.argument_types) ||
595
608
  compareStrings(a.fn.return_type, b.fn.return_type));
596
609
  }
597
610
  return Object.entries(schemaFunctionsGroupedByName)
@@ -734,7 +747,7 @@ export type CompositeTypes<
734
747
 
735
748
  export const Constants = {
736
749
  ${schemas.map((schema) => {
737
- const schemaEnums = introspectionBySchema[schema.name].enums;
750
+ const schemaEnums = getSchemaIntrospection(schema.name).enums;
738
751
  return `${JSON.stringify(schema.name)}: {
739
752
  Enums: {
740
753
  ${schemaEnums.map((enum_) => `${JSON.stringify(enum_.name)}: [${enum_.enums
@@ -795,18 +808,17 @@ typeSchema) => {
795
808
  else {
796
809
  const preferredSchema = typeSchema ?? schema.name;
797
810
  const enumTypes = types.filter((type) => type.name === pgType && type.enums.length > 0);
798
- if (enumTypes.length > 0) {
799
- const enumType = enumTypes.find((type) => type.schema === preferredSchema) ||
800
- enumTypes[0];
811
+ const enumType = enumTypes.find((type) => type.schema === preferredSchema) ?? enumTypes[0];
812
+ if (enumType) {
801
813
  if (schemas.some(({ name }) => name === enumType.schema)) {
802
814
  return `Database[${JSON.stringify(enumType.schema)}]['Enums'][${JSON.stringify(enumType.name)}]`;
803
815
  }
804
816
  return enumType.enums.map((variant) => JSON.stringify(variant)).join("|");
805
817
  }
806
818
  const compositeTypes = types.filter((type) => type.name === pgType && type.attributes.length > 0);
807
- if (compositeTypes.length > 0) {
808
- const compositeType = compositeTypes.find((type) => type.schema === preferredSchema) ||
809
- compositeTypes[0];
819
+ const compositeType = compositeTypes.find((type) => type.schema === preferredSchema) ??
820
+ compositeTypes[0];
821
+ if (compositeType) {
810
822
  if (schemas.some(({ name }) => name === compositeType.schema)) {
811
823
  return `Database[${JSON.stringify(compositeType.schema)}]['CompositeTypes'][${JSON.stringify(compositeType.name)}]`;
812
824
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/postgrest-typegen",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Type generation for PostgREST from PostgreSQL schemas",
5
5
  "keywords": [
6
6
  "codegen",
@@ -55,29 +55,39 @@
55
55
  "scripts": {
56
56
  "build": "tsc --project tsconfig.build.json",
57
57
  "check-types": "tsc --noEmit",
58
+ "check-types:consumer": "tsc --project tsconfig.consumer.json",
58
59
  "format-and-lint": "oxfmt --check . && oxlint --deny-warnings src test scripts",
59
60
  "format-and-lint:fix": "oxfmt . && oxlint --fix src test scripts",
60
61
  "knip": "knip",
61
62
  "test": "bun scripts/run-tests.ts test/"
62
63
  },
63
64
  "dependencies": {
64
- "arktype": "2.2.3",
65
- "oxfmt": "0.66.0"
65
+ "arktype": "2.2.3"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@testcontainers/postgresql": "^12.1.0",
69
+ "@tsconfig/bun": "^1.0.11",
69
70
  "@tsconfig/node-ts": "^23.6.4",
70
71
  "@tsconfig/node24": "^24.0.5",
71
72
  "@types/bun": "^1.4.0",
72
73
  "@types/node": "^26.3.0",
73
74
  "@types/pg": "^8.23.1",
74
75
  "knip": "^6.32.3",
76
+ "oxfmt": "0.66.0",
75
77
  "oxlint": "1.83.0",
76
78
  "oxlint-tsgolint": "7.0.2002",
77
79
  "pg": "^8.23.0",
78
80
  "testcontainers": "^12.1.0",
79
81
  "typescript": "7.0.2"
80
82
  },
83
+ "peerDependencies": {
84
+ "oxfmt": "0.66.0"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "oxfmt": {
88
+ "optional": true
89
+ }
90
+ },
81
91
  "engines": {
82
92
  "node": ">=22.12.0"
83
93
  }
@@ -24,8 +24,7 @@ export const generateGo = ({
24
24
  }: GeneratorMetadata): string => {
25
25
  const columnsByTableId = columns.reduce(
26
26
  (acc, curr) => {
27
- acc[curr.table_id] ??= [];
28
- acc[curr.table_id].push(curr);
27
+ (acc[curr.table_id] ??= []).push(curr);
29
28
  return acc;
30
29
  },
31
30
  {} as Record<string, PostgresColumn[]>,
@@ -110,7 +109,7 @@ function formatForGoTypeName(name: string): string {
110
109
  .split(/[^a-zA-Z0-9]/)
111
110
  .map((word) => {
112
111
  if (word) {
113
- return `${word[0].toUpperCase()}${word.slice(1)}`;
112
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
114
113
  } else {
115
114
  return "";
116
115
  }
@@ -93,8 +93,7 @@ class PythonContext {
93
93
  );
94
94
  this.columns = columns.reduce(
95
95
  (acc, curr) => {
96
- acc[curr.table_id] ??= [];
97
- acc[curr.table_id].push(curr);
96
+ (acc[curr.table_id] ??= []).push(curr);
98
97
  return acc;
99
98
  },
100
99
  {} as Record<number, PostgresColumn[]>,
@@ -109,16 +108,28 @@ class PythonContext {
109
108
  );
110
109
  }
111
110
 
111
+ getSchema(name: string): PostgresSchema {
112
+ const schema = this.schemas[name];
113
+ if (!schema) {
114
+ throw new Error(
115
+ `Schema ${JSON.stringify(name)} is missing from the generator metadata`,
116
+ );
117
+ }
118
+ return schema;
119
+ }
120
+
112
121
  resolveTypeName(name: string, typeSchema: string): string {
113
122
  const qualified = qualifiedTypeName(typeSchema, name);
114
- if (qualified in this.user_enums) {
115
- return this.user_enums[qualified].name;
123
+ const userEnum = this.user_enums[qualified];
124
+ if (userEnum) {
125
+ return userEnum.name;
116
126
  }
117
- if (name in PY_TYPE_MAP) {
118
- return PY_TYPE_MAP[name];
127
+ const mappedType = PY_TYPE_MAP[name];
128
+ if (mappedType !== undefined) {
129
+ return mappedType;
119
130
  }
120
- if (qualified in this.types) {
121
- const type = this.types[qualified];
131
+ const type = this.types[qualified];
132
+ if (type) {
122
133
  return `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}`;
123
134
  }
124
135
  return "Any";
@@ -158,7 +169,7 @@ class PythonContext {
158
169
  },
159
170
  );
160
171
 
161
- const schema = this.schemas[type.schema];
172
+ const schema = this.getSchema(type.schema);
162
173
  return new PythonBaseModel(type.name, schema, attributeEntries);
163
174
  }
164
175
 
@@ -192,7 +203,7 @@ class PythonContext {
192
203
  tableToClass(
193
204
  table: PostgresTable,
194
205
  ): [PythonBaseModel, PythonTypedDict, PythonTypedDict] {
195
- const schema = this.schemas[table.schema];
206
+ const schema = this.getSchema(table.schema);
196
207
  const select = new PythonBaseModel(
197
208
  table.name,
198
209
  schema,
@@ -217,7 +228,7 @@ class PythonContext {
217
228
  const attributes = this.columnsToClassAttrs(view.id);
218
229
  return new PythonBaseModel(
219
230
  view.name,
220
- this.schemas[view.schema],
231
+ this.getSchema(view.schema),
221
232
  attributes,
222
233
  );
223
234
  }
@@ -226,7 +237,7 @@ class PythonContext {
226
237
  const attributes = this.columnsToClassAttrs(matview.id);
227
238
  return new PythonBaseModel(
228
239
  matview.name,
229
- this.schemas[matview.schema],
240
+ this.getSchema(matview.schema),
230
241
  attributes,
231
242
  );
232
243
  }
@@ -463,7 +474,7 @@ function formatForPyClassName(name: string): string {
463
474
  .split(/[^a-zA-Z0-9]/)
464
475
  .map((word) => {
465
476
  if (word) {
466
- return `${word[0].toUpperCase()}${word.slice(1)}`;
477
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
467
478
  } else {
468
479
  return "";
469
480
  }
@@ -303,9 +303,9 @@ export const generateSwift = (
303
303
  ]),
304
304
  );
305
305
 
306
- columns
307
- .filter((c) => c.table_id in columnsByTableId)
308
- .forEach((c) => columnsByTableId[c.table_id].push(c));
306
+ for (const column of columns) {
307
+ columnsByTableId[column.table_id]?.push(column);
308
+ }
309
309
 
310
310
  let output = [
311
311
  "import Foundation",
@@ -492,7 +492,7 @@ function formatForSwiftTypeName(name: string): string {
492
492
  .split(/[^a-zA-Z0-9]+/)
493
493
  .map((word) => {
494
494
  if (word) {
495
- return `${word[0].toUpperCase()}${word.slice(1)}`;
495
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
496
496
  } else {
497
497
  return "";
498
498
  }
@@ -3,7 +3,6 @@
3
3
  // literals (relying on Array#toString) to build the output. Preserving that is required for
4
4
  // byte-parity, and the offending expressions sit inside template literals where a targeted
5
5
  // disable comment would corrupt the emitted string.
6
- import { format as oxfmtFormat } from "oxfmt";
7
6
  import type {
8
7
  GeneratorMetadata,
9
8
  PostgresColumn,
@@ -39,16 +38,30 @@ export interface GenerateTypescriptOptions {
39
38
  */
40
39
  defaultSchema?: string;
41
40
  /**
42
- * Formatter used on the generated output. Defaults to `oxfmt`, called
43
- * inline. Callers on a latency-sensitive request path (e.g. postgres-meta's
44
- * hosted `/generators/typescript` route) can substitute a worker-pool-backed
41
+ * Formatter used on the generated output. Defaults to `oxfmt`, an optional
42
+ * peer dependency that is imported only when this default runs, so callers
43
+ * that always pass their own formatter need not install it. Callers on a
44
+ * latency-sensitive request path (e.g. postgres-meta's hosted
45
+ * `/generators/typescript` route) can substitute a worker-pool-backed
45
46
  * formatter here instead of blocking the event loop on every call.
46
47
  */
47
48
  format?: (code: string) => Promise<string>;
48
49
  }
49
50
 
51
+ const loadOxfmt = async () => {
52
+ try {
53
+ return await import("oxfmt");
54
+ } catch (error) {
55
+ throw new Error(
56
+ "oxfmt is not installed. It is an optional peer dependency that only the default formatter needs: install it, or pass a `format` option to generateTypescript.",
57
+ { cause: error },
58
+ );
59
+ }
60
+ };
61
+
50
62
  const defaultFormat = async (code: string): Promise<string> => {
51
- const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
63
+ const { format } = await loadOxfmt();
64
+ const { code: formatted, errors } = await format("output.ts", code, {
52
65
  semi: false,
53
66
  printWidth: 80,
54
67
  });
@@ -162,10 +175,26 @@ export const generateTypescript = async (
162
175
  tablesNamesByTableId[tableLike.id] = tableLike.name;
163
176
  }
164
177
  for (const column of columns) {
165
- if (column.table_id in columnsByTableId) {
166
- columnsByTableId[column.table_id].push(column);
167
- }
178
+ columnsByTableId[column.table_id]?.push(column);
168
179
  }
180
+ const getColumns = (tableId: number): PostgresColumn[] => {
181
+ const tableColumns = columnsByTableId[tableId];
182
+ if (!tableColumns) {
183
+ throw new Error(
184
+ `Relation ${tableId} is missing from the generator metadata`,
185
+ );
186
+ }
187
+ return tableColumns;
188
+ };
189
+ const getSchemaIntrospection = (schemaName: string) => {
190
+ const schemaIntrospection = introspectionBySchema[schemaName];
191
+ if (!schemaIntrospection) {
192
+ throw new Error(
193
+ `Schema ${JSON.stringify(schemaName)} is missing from the generator metadata`,
194
+ );
195
+ }
196
+ return schemaIntrospection;
197
+ };
169
198
 
170
199
  for (const type of types) {
171
200
  typesById.set(type.id, type);
@@ -173,13 +202,11 @@ export const generateTypescript = async (
173
202
  if (type.type_relation_id) {
174
203
  relationTypeByIds.set(type.id, type);
175
204
  }
176
- if (type.schema in introspectionBySchema) {
177
- if (type.enums.length > 0) {
178
- introspectionBySchema[type.schema].enums.push(type);
179
- }
180
- if (type.attributes.length > 0) {
181
- introspectionBySchema[type.schema].compositeTypes.push(type);
182
- }
205
+ if (type.enums.length > 0) {
206
+ introspectionBySchema[type.schema]?.enums.push(type);
207
+ }
208
+ if (type.attributes.length > 0) {
209
+ introspectionBySchema[type.schema]?.compositeTypes.push(type);
183
210
  }
184
211
  }
185
212
 
@@ -233,41 +260,33 @@ export const generateTypescript = async (
233
260
  }
234
261
 
235
262
  for (const table of tables) {
236
- if (table.schema in introspectionBySchema) {
237
- introspectionBySchema[table.schema].tables.push({
238
- table,
239
- relationships: getRelationships(table),
240
- });
241
- }
263
+ introspectionBySchema[table.schema]?.tables.push({
264
+ table,
265
+ relationships: getRelationships(table),
266
+ });
242
267
  }
243
268
  for (const table of foreignTables) {
244
- if (table.schema in introspectionBySchema) {
245
- introspectionBySchema[table.schema].tables.push({
246
- table,
247
- relationships: getRelationships(table),
248
- });
249
- }
269
+ introspectionBySchema[table.schema]?.tables.push({
270
+ table,
271
+ relationships: getRelationships(table),
272
+ });
250
273
  }
251
274
  for (const view of views) {
252
- if (view.schema in introspectionBySchema) {
253
- introspectionBySchema[view.schema].views.push({
254
- view,
255
- relationships: getRelationships(view),
256
- });
257
- }
275
+ introspectionBySchema[view.schema]?.views.push({
276
+ view,
277
+ relationships: getRelationships(view),
278
+ });
258
279
  }
259
280
  for (const materializedView of materializedViews) {
260
- if (materializedView.schema in introspectionBySchema) {
261
- introspectionBySchema[materializedView.schema].views.push({
262
- view: {
263
- ...materializedView,
264
- is_updatable: false,
265
- is_insert_enabled: false,
266
- is_update_enabled: false,
267
- },
268
- relationships: getRelationships(materializedView),
269
- });
270
- }
281
+ introspectionBySchema[materializedView.schema]?.views.push({
282
+ view: {
283
+ ...materializedView,
284
+ is_updatable: false,
285
+ is_insert_enabled: false,
286
+ is_update_enabled: false,
287
+ },
288
+ relationships: getRelationships(materializedView),
289
+ });
271
290
  }
272
291
  // Helper function to get table/view name from relation id
273
292
  const getTableNameFromRelationId = (
@@ -276,20 +295,22 @@ export const generateTypescript = async (
276
295
  ): string | null => {
277
296
  if (!relationId) return null;
278
297
 
279
- if (tablesNamesByTableId[relationId])
280
- return tablesNamesByTableId[relationId];
298
+ const tableName = tablesNamesByTableId[relationId];
299
+ if (tableName) return tableName;
281
300
  // if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type
282
301
  const reltype = returnTypeId ? relationTypeByIds.get(returnTypeId) : null;
283
302
  return reltype ? reltype.name : null;
284
303
  };
285
304
 
286
305
  for (const func of functions) {
287
- if (func.schema in introspectionBySchema) {
306
+ const schemaIntrospection = introspectionBySchema[func.schema];
307
+ if (schemaIntrospection) {
288
308
  // Get all input args (in, inout, variadic modes)
289
309
  const inArgs = func.args.filter(({ mode }) =>
290
310
  VALID_FUNCTION_ARGS_MODE.has(mode),
291
311
  );
292
312
 
313
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
293
314
  if (
294
315
  // Case 1: Function has no parameters
295
316
  inArgs.length === 0 ||
@@ -308,23 +329,23 @@ export const generateTypescript = async (
308
329
  // Case 4: Single unnamed parameter of valid type (json, jsonb, text)
309
330
  // Exclude all functions definitions that have only one single argument unnamed argument that isn't
310
331
  // a json/jsonb/text as it won't be considered by PostgREST
311
- (inArgs.length === 1 &&
312
- inArgs[0].name === "" &&
313
- (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) ||
332
+ (soleInArg &&
333
+ soleInArg.name === "" &&
334
+ (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(soleInArg.type_id) ||
314
335
  // OR if the function have a single unnamed args which is another table (embeded function)
315
- (relationTypeByIds.get(inArgs[0].type_id) &&
336
+ (relationTypeByIds.get(soleInArg.type_id) &&
316
337
  getTableNameFromRelationId(
317
338
  func.return_type_relation_id,
318
339
  func.return_type_id,
319
340
  )) ||
320
341
  // OR if the function takes a table row but doesn't qualify as embedded (for error reporting)
321
- (relationTypeByIds.get(inArgs[0].type_id) &&
342
+ (relationTypeByIds.get(soleInArg.type_id) &&
322
343
  !getTableNameFromRelationId(
323
344
  func.return_type_relation_id,
324
345
  func.return_type_id,
325
346
  ))))
326
347
  ) {
327
- introspectionBySchema[func.schema].functions.push({ fn: func, inArgs });
348
+ schemaIntrospection.functions.push({ fn: func, inArgs });
328
349
  }
329
350
  }
330
351
  }
@@ -332,20 +353,18 @@ export const generateTypescript = async (
332
353
  // combines tables + foreign tables, `.views` combines views + materialized
333
354
  // views — so the cross-collection name sort can't be expressed by the
334
355
  // single-collection `sortGeneratorMetadata` pass and stays here.
335
- for (const schema in introspectionBySchema) {
336
- introspectionBySchema[schema].tables.sort((a, b) =>
356
+ for (const schemaIntrospection of Object.values(introspectionBySchema)) {
357
+ schemaIntrospection.tables.sort((a, b) =>
337
358
  compareStrings(a.table.name, b.table.name),
338
359
  );
339
- introspectionBySchema[schema].views.sort((a, b) =>
360
+ schemaIntrospection.views.sort((a, b) =>
340
361
  compareStrings(a.view.name, b.view.name),
341
362
  );
342
- introspectionBySchema[schema].functions.sort((a, b) =>
363
+ schemaIntrospection.functions.sort((a, b) =>
343
364
  compareStrings(a.fn.name, b.fn.name),
344
365
  );
345
- introspectionBySchema[schema].enums.sort((a, b) =>
346
- compareStrings(a.name, b.name),
347
- );
348
- introspectionBySchema[schema].compositeTypes.sort((a, b) =>
366
+ schemaIntrospection.enums.sort((a, b) => compareStrings(a.name, b.name));
367
+ schemaIntrospection.compositeTypes.sort((a, b) =>
349
368
  compareStrings(a.name, b.name),
350
369
  );
351
370
  }
@@ -376,8 +395,9 @@ export const generateTypescript = async (
376
395
  }
377
396
  // Case 2: if the function has a single table argument, we need to add SetofOptions to allow selecting sub fields of the table
378
397
  // and set the right "from" and "to" values to allow selecting from a table row
379
- if (fn.args.length === 1) {
380
- const relationType = relationTypeByIds.get(fn.args[0].type_id);
398
+ const soleArg = fn.args.length === 1 ? fn.args[0] : undefined;
399
+ if (soleArg) {
400
+ const relationType = relationTypeByIds.get(soleArg.type_id);
381
401
 
382
402
  // Only add SetofOptions for functions with table arguments (embedded functions)
383
403
  // or specific functions that RETURNS table-name
@@ -445,7 +465,7 @@ export const generateTypescript = async (
445
465
  )?.view;
446
466
  if (relation) {
447
467
  return `{
448
- ${columnsByTableId[relation.id]
468
+ ${getColumns(relation.id)
449
469
  .map((column) =>
450
470
  generateColumnTsDefinition(
451
471
  schema,
@@ -476,10 +496,11 @@ export const generateTypescript = async (
476
496
  fn: PostgresFunction,
477
497
  inArgs: PostgresFunction["args"],
478
498
  ) => {
499
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
479
500
  if (
480
- inArgs.length === 1 &&
481
- inArgs[0].name === "" &&
482
- relationTypeByIds.get(inArgs[0].type_id) &&
501
+ soleInArg &&
502
+ soleInArg.name === "" &&
503
+ relationTypeByIds.get(soleInArg.type_id) &&
483
504
  !getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id)
484
505
  ) {
485
506
  return true;
@@ -496,6 +517,7 @@ export const generateTypescript = async (
496
517
  ) => {
497
518
  // If there is a single function definition, there is no conflict
498
519
  if (fns.length <= 1) return null;
520
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
499
521
 
500
522
  // Generic conflict detection patterns
501
523
  // Pattern 1: No-args vs default-args conflicts
@@ -503,16 +525,18 @@ export const generateTypescript = async (
503
525
  const conflictingFns = fns.filter(
504
526
  ({ fn: otherFn, inArgs: otherInArgs }) => {
505
527
  if (otherFn === fn) return false;
528
+ const otherSoleInArg =
529
+ otherInArgs.length === 1 ? otherInArgs[0] : undefined;
506
530
  return (
507
- otherInArgs.length === 1 &&
508
- otherInArgs[0].name === "" &&
509
- otherInArgs[0].has_default
531
+ otherSoleInArg &&
532
+ otherSoleInArg.name === "" &&
533
+ otherSoleInArg.has_default
510
534
  );
511
535
  },
512
536
  );
513
537
 
514
- if (conflictingFns.length > 0) {
515
- const conflictingFn = conflictingFns[0];
538
+ const conflictingFn = conflictingFns[0];
539
+ if (conflictingFn) {
516
540
  const returnTypeName =
517
541
  typesById.get(conflictingFn.fn.return_type_id)?.name || "unknown";
518
542
  return `Could not choose the best candidate function between: ${schema.name}.${fn.name}(), ${schema.name}.${fn.name}( => ${returnTypeName}). Try renaming the parameters or the function itself in the database so function overloading can be resolved`;
@@ -520,14 +544,16 @@ export const generateTypescript = async (
520
544
  }
521
545
 
522
546
  // Pattern 2: Same parameter name but different types (unresolvable overloads)
523
- if (inArgs.length === 1 && inArgs[0].name !== "") {
547
+ if (soleInArg && soleInArg.name !== "") {
524
548
  const conflictingFns = fns.filter(
525
549
  ({ fn: otherFn, inArgs: otherInArgs }) => {
526
550
  if (otherFn === fn) return false;
551
+ const otherSoleInArg =
552
+ otherInArgs.length === 1 ? otherInArgs[0] : undefined;
527
553
  return (
528
- otherInArgs.length === 1 &&
529
- otherInArgs[0].name === inArgs[0].name &&
530
- otherInArgs[0].type_id !== inArgs[0].type_id
554
+ otherSoleInArg &&
555
+ otherSoleInArg.name === soleInArg.name &&
556
+ otherSoleInArg.type_id !== soleInArg.type_id
531
557
  );
532
558
  },
533
559
  );
@@ -711,11 +737,12 @@ export const generateTypescript = async (
711
737
  inArgs: PostgresFunction["args"],
712
738
  relation: { id: number; name: string },
713
739
  ) => {
714
- if (inArgs.length === 1) {
740
+ const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
741
+ if (soleInArg) {
715
742
  // `relationTypeByIds` is already "types backed by a relation", so a hit
716
743
  // identifies the relation regardless of how the argument was spelled.
717
744
  // A `variadic` argument is an array type and never appears here.
718
- const argType = relationTypeByIds.get(inArgs[0]!.type_id);
745
+ const argType = relationTypeByIds.get(soleInArg.type_id);
719
746
  if (argType) {
720
747
  return argType.type_relation_id === relation.id;
721
748
  }
@@ -738,7 +765,7 @@ export type Database = {
738
765
  functions: schemaFunctions,
739
766
  enums: schemaEnums,
740
767
  compositeTypes: schemaCompositeTypes,
741
- } = introspectionBySchema[schema.name];
768
+ } = getSchemaIntrospection(schema.name);
742
769
  return `${JSON.stringify(schema.name)}: {
743
770
  Tables: {
744
771
  ${
@@ -751,7 +778,7 @@ export type Database = {
751
778
  }) => `${JSON.stringify(table.name)}: {
752
779
  Row: {
753
780
  ${[
754
- ...columnsByTableId[table.id].map((column) =>
781
+ ...getColumns(table.id).map((column) =>
755
782
  generateColumnTsDefinition(
756
783
  schema,
757
784
  {
@@ -777,7 +804,7 @@ export type Database = {
777
804
  ]}
778
805
  }
779
806
  Insert: {
780
- ${columnsByTableId[table.id].map((column) => {
807
+ ${getColumns(table.id).map((column) => {
781
808
  if (
782
809
  column.identity_generation === "ALWAYS" ||
783
810
  column.is_generated
@@ -801,7 +828,7 @@ export type Database = {
801
828
  })}
802
829
  }
803
830
  Update: {
804
- ${columnsByTableId[table.id].map((column) => {
831
+ ${getColumns(table.id).map((column) => {
805
832
  if (
806
833
  column.identity_generation === "ALWAYS" ||
807
834
  column.is_generated
@@ -840,7 +867,7 @@ export type Database = {
840
867
  }) => `${JSON.stringify(view.name)}: {
841
868
  Row: {
842
869
  ${[
843
- ...columnsByTableId[view.id].map((column) =>
870
+ ...getColumns(view.id).map((column) =>
844
871
  generateColumnTsDefinition(
845
872
  schema,
846
873
  {
@@ -871,7 +898,7 @@ export type Database = {
871
898
  // version 1 documents) falls back to the old gate.
872
899
  (view.is_insert_enabled ?? view.is_updatable)
873
900
  ? `Insert: {
874
- ${columnsByTableId[view.id].map((column) => {
901
+ ${getColumns(view.id).map((column) => {
875
902
  if (!column.is_updatable) {
876
903
  return `${JSON.stringify(column.name)}?: never`;
877
904
  }
@@ -893,7 +920,7 @@ export type Database = {
893
920
  }${
894
921
  (view.is_update_enabled ?? view.is_updatable)
895
922
  ? `Update: {
896
- ${columnsByTableId[view.id].map((column) => {
923
+ ${getColumns(view.id).map((column) => {
897
924
  if (!column.is_updatable) {
898
925
  return `${JSON.stringify(column.name)}?: never`;
899
926
  }
@@ -926,14 +953,13 @@ export type Database = {
926
953
  }
927
954
  const schemaFunctionsGroupedByName = schemaFunctions.reduce(
928
955
  (acc, curr) => {
929
- acc[curr.fn.name] ??= [];
930
- acc[curr.fn.name].push(curr);
956
+ (acc[curr.fn.name] ??= []).push(curr);
931
957
  return acc;
932
958
  },
933
959
  {} as Record<string, typeof schemaFunctions>,
934
960
  );
935
- for (const fnName in schemaFunctionsGroupedByName) {
936
- schemaFunctionsGroupedByName[fnName].sort(
961
+ for (const fns of Object.values(schemaFunctionsGroupedByName)) {
962
+ fns.sort(
937
963
  (a, b) =>
938
964
  compareStrings(a.fn.argument_types, b.fn.argument_types) ||
939
965
  compareStrings(a.fn.return_type, b.fn.return_type),
@@ -1100,7 +1126,7 @@ export type CompositeTypes<
1100
1126
 
1101
1127
  export const Constants = {
1102
1128
  ${schemas.map((schema) => {
1103
- const schemaEnums = introspectionBySchema[schema.name].enums;
1129
+ const schemaEnums = getSchemaIntrospection(schema.name).enums;
1104
1130
  return `${JSON.stringify(schema.name)}: {
1105
1131
  Enums: {
1106
1132
  ${schemaEnums.map(
@@ -1176,10 +1202,9 @@ export const pgTypeToTsType = (
1176
1202
  const enumTypes = types.filter(
1177
1203
  (type) => type.name === pgType && type.enums.length > 0,
1178
1204
  );
1179
- if (enumTypes.length > 0) {
1180
- const enumType =
1181
- enumTypes.find((type) => type.schema === preferredSchema) ||
1182
- enumTypes[0];
1205
+ const enumType =
1206
+ enumTypes.find((type) => type.schema === preferredSchema) ?? enumTypes[0];
1207
+ if (enumType) {
1183
1208
  if (schemas.some(({ name }) => name === enumType.schema)) {
1184
1209
  return `Database[${JSON.stringify(enumType.schema)}]['Enums'][${JSON.stringify(
1185
1210
  enumType.name,
@@ -1191,10 +1216,10 @@ export const pgTypeToTsType = (
1191
1216
  const compositeTypes = types.filter(
1192
1217
  (type) => type.name === pgType && type.attributes.length > 0,
1193
1218
  );
1194
- if (compositeTypes.length > 0) {
1195
- const compositeType =
1196
- compositeTypes.find((type) => type.schema === preferredSchema) ||
1197
- compositeTypes[0];
1219
+ const compositeType =
1220
+ compositeTypes.find((type) => type.schema === preferredSchema) ??
1221
+ compositeTypes[0];
1222
+ if (compositeType) {
1198
1223
  if (schemas.some(({ name }) => name === compositeType.schema)) {
1199
1224
  return `Database[${JSON.stringify(
1200
1225
  compositeType.schema,