@supabase/postgrest-typegen 0.3.1 → 0.5.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.
@@ -7,18 +7,17 @@ import type {
7
7
  GeneratorMetadata,
8
8
  PostgresColumn,
9
9
  PostgresForeignTable,
10
- PostgresFunction,
11
10
  PostgresMaterializedView,
12
11
  PostgresSchema,
13
12
  PostgresTable,
14
13
  PostgresType,
15
14
  PostgresView,
15
+ RpcFunction,
16
+ RpcFunctionOverload,
17
+ RpcTypeReference,
16
18
  } from "../types.ts";
17
- import {
18
- VALID_FUNCTION_ARGS_MODE,
19
- VALID_UNNAMED_FUNCTION_ARG_TYPES,
20
- } from "./constants.ts";
21
19
  import { compareStrings } from "../collation.ts";
20
+ import { resolveRpcFunctions } from "../rpc.ts";
22
21
 
23
22
  export interface GenerateTypescriptOptions {
24
23
  /**
@@ -34,7 +33,8 @@ export interface GenerateTypescriptOptions {
34
33
  /**
35
34
  * Schema treated as the default for the generated `Tables`/`Enums`/etc.
36
35
  * helper types. Replaces postgres-meta's `GENERATE_TYPES_DEFAULT_SCHEMA`
37
- * env read. Default `'public'`.
36
+ * env read. Default `'public'`. When it is not among the generated schemas,
37
+ * the first generated schema is used instead, so the helpers keep resolving.
38
38
  */
39
39
  defaultSchema?: string;
40
40
  /**
@@ -119,15 +119,20 @@ export const generateTypescript = async (
119
119
  materializedViews,
120
120
  columns,
121
121
  relationships,
122
- functions,
123
122
  types,
124
123
  } = metadata;
124
+ // Documents predating the collection (still valid version 1) resolve it here.
125
+ const rpcFunctions = metadata.rpcFunctions ?? resolveRpcFunctions(metadata);
125
126
  const {
126
127
  detectOneToOneRelationships = false,
127
128
  postgrestVersion,
128
129
  defaultSchema = "public",
129
130
  format = defaultFormat,
130
131
  } = opts;
132
+ const resolvedDefaultSchema =
133
+ schemas.some(({ name }) => name === defaultSchema) || !schemas[0]
134
+ ? defaultSchema
135
+ : schemas[0].name;
131
136
  // Every relation-typed value resolves against the same collections, so build
132
137
  // the lookup context once rather than reassembling it at each call site.
133
138
  const typeContext: TypeResolutionContext = {
@@ -149,7 +154,7 @@ export const generateTypescript = async (
149
154
  view: PostgresView;
150
155
  relationships: TsRelationship[];
151
156
  }[];
152
- functions: { fn: PostgresFunction; inArgs: PostgresFunction["args"] }[];
157
+ functions: RpcFunction[];
153
158
  enums: PostgresType[];
154
159
  compositeTypes: PostgresType[];
155
160
  }>(
@@ -159,8 +164,7 @@ export const generateTypescript = async (
159
164
  ]),
160
165
  );
161
166
  const columnsByTableId: Record<number, PostgresColumn[]> = {};
162
- const tablesNamesByTableId: Record<number, string> = {};
163
- const relationTypeByIds = new Map<number, (typeof types)[number]>();
167
+ const relationsById = new Map<number, { id: number; schema: string }>();
164
168
  // group types by id for quicker lookup
165
169
  const typesById = new Map<number, (typeof types)[number]>();
166
170
  const tablesLike = [
@@ -172,7 +176,7 @@ export const generateTypescript = async (
172
176
 
173
177
  for (const tableLike of tablesLike) {
174
178
  columnsByTableId[tableLike.id] = [];
175
- tablesNamesByTableId[tableLike.id] = tableLike.name;
179
+ relationsById.set(tableLike.id, tableLike);
176
180
  }
177
181
  for (const column of columns) {
178
182
  columnsByTableId[column.table_id]?.push(column);
@@ -198,10 +202,6 @@ export const generateTypescript = async (
198
202
 
199
203
  for (const type of types) {
200
204
  typesById.set(type.id, type);
201
- // Save all the types that are relation types for quicker lookup
202
- if (type.type_relation_id) {
203
- relationTypeByIds.set(type.id, type);
204
- }
205
205
  if (type.enums.length > 0) {
206
206
  introspectionBySchema[type.schema]?.enums.push(type);
207
207
  }
@@ -288,66 +288,8 @@ export const generateTypescript = async (
288
288
  relationships: getRelationships(materializedView),
289
289
  });
290
290
  }
291
- // Helper function to get table/view name from relation id
292
- const getTableNameFromRelationId = (
293
- relationId: number | null,
294
- returnTypeId: number | null,
295
- ): string | null => {
296
- if (!relationId) return null;
297
-
298
- const tableName = tablesNamesByTableId[relationId];
299
- if (tableName) return tableName;
300
- // if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type
301
- const reltype = returnTypeId ? relationTypeByIds.get(returnTypeId) : null;
302
- return reltype ? reltype.name : null;
303
- };
304
-
305
- for (const func of functions) {
306
- const schemaIntrospection = introspectionBySchema[func.schema];
307
- if (schemaIntrospection) {
308
- // Get all input args (in, inout, variadic modes)
309
- const inArgs = func.args.filter(({ mode }) =>
310
- VALID_FUNCTION_ARGS_MODE.has(mode),
311
- );
312
-
313
- const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
314
- if (
315
- // Case 1: Function has no parameters
316
- inArgs.length === 0 ||
317
- // Case 2: All input args are named
318
- !inArgs.some(({ name }) => name === "") ||
319
- // Case 3: All unnamed args have default values AND are valid types
320
- inArgs.every((arg) => {
321
- if (arg.name === "") {
322
- return (
323
- arg.has_default &&
324
- VALID_UNNAMED_FUNCTION_ARG_TYPES.has(arg.type_id)
325
- );
326
- }
327
- return true;
328
- }) ||
329
- // Case 4: Single unnamed parameter of valid type (json, jsonb, text)
330
- // Exclude all functions definitions that have only one single argument unnamed argument that isn't
331
- // a json/jsonb/text as it won't be considered by PostgREST
332
- (soleInArg &&
333
- soleInArg.name === "" &&
334
- (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(soleInArg.type_id) ||
335
- // OR if the function have a single unnamed args which is another table (embeded function)
336
- (relationTypeByIds.get(soleInArg.type_id) &&
337
- getTableNameFromRelationId(
338
- func.return_type_relation_id,
339
- func.return_type_id,
340
- )) ||
341
- // OR if the function takes a table row but doesn't qualify as embedded (for error reporting)
342
- (relationTypeByIds.get(soleInArg.type_id) &&
343
- !getTableNameFromRelationId(
344
- func.return_type_relation_id,
345
- func.return_type_id,
346
- ))))
347
- ) {
348
- schemaIntrospection.functions.push({ fn: func, inArgs });
349
- }
350
- }
291
+ for (const fn of rpcFunctions) {
292
+ introspectionBySchema[fn.schema]?.functions.push(fn);
351
293
  }
352
294
  // These per-schema groups MERGE multiple metadata collections — `.tables`
353
295
  // combines tables + foreign tables, `.views` combines views + materialized
@@ -360,110 +302,62 @@ export const generateTypescript = async (
360
302
  schemaIntrospection.views.sort((a, b) =>
361
303
  compareStrings(a.view.name, b.view.name),
362
304
  );
363
- schemaIntrospection.functions.sort((a, b) =>
364
- compareStrings(a.fn.name, b.fn.name),
365
- );
366
305
  schemaIntrospection.enums.sort((a, b) => compareStrings(a.name, b.name));
367
306
  schemaIntrospection.compositeTypes.sort((a, b) =>
368
307
  compareStrings(a.name, b.name),
369
308
  );
370
309
  }
371
310
 
311
+ const referencedTsType = (
312
+ schema: PostgresSchema,
313
+ { format, type_schema }: RpcTypeReference,
314
+ ) =>
315
+ format === null || type_schema === null
316
+ ? "unknown"
317
+ : pgTypeToTsType(schema, format, typeContext, type_schema);
318
+
372
319
  const getFunctionTsReturnType = (
373
- fn: PostgresFunction,
320
+ overload: RpcFunctionOverload,
374
321
  returnType: string,
375
322
  ) => {
376
- // Determine if this function should have SetofOptions
377
- let setofOptionsInfo = "";
378
-
379
- const returnTableName = getTableNameFromRelationId(
380
- fn.return_type_relation_id,
381
- fn.return_type_id,
382
- );
383
- const returnsSetOfTable =
384
- fn.is_set_returning_function && fn.return_type_relation_id !== null;
385
- const returnsMultipleRows = fn.prorows !== null && fn.prorows > 1;
386
- // Case 1: if the function returns a table, we need to add SetofOptions to allow selecting sub fields of the table
387
- // Those can be used in rpc to select sub fields of a table
388
- if (returnTableName) {
389
- setofOptionsInfo = `SetofOptions: {
390
- from: "*"
391
- to: ${JSON.stringify(returnTableName)}
392
- isOneToOne: ${Boolean(!returnsMultipleRows)}
393
- isSetofReturn: ${fn.is_set_returning_function}
394
- }`;
395
- }
396
- // Case 2: if the function has a single table argument, we need to add SetofOptions to allow selecting sub fields of the table
397
- // and set the right "from" and "to" values to allow selecting from a table row
398
- const soleArg = fn.args.length === 1 ? fn.args[0] : undefined;
399
- if (soleArg) {
400
- const relationType = relationTypeByIds.get(soleArg.type_id);
401
-
402
- // Only add SetofOptions for functions with table arguments (embedded functions)
403
- // or specific functions that RETURNS table-name
404
- if (relationType) {
405
- const sourceTable = relationType.format;
406
- // Case 1: Standard embedded function with proper setof detection
407
- if (returnsSetOfTable && returnTableName) {
408
- setofOptionsInfo = `SetofOptions: {
409
- from: ${JSON.stringify(sourceTable)}
410
- to: ${JSON.stringify(returnTableName)}
411
- isOneToOne: ${Boolean(!returnsMultipleRows)}
412
- isSetofReturn: true
413
- }`;
414
- } // Case 2: Handle RETURNS table-name those are always a one to one relationship
415
- else if (returnTableName && !returnsSetOfTable) {
416
- const targetTable = returnTableName;
417
- setofOptionsInfo = `SetofOptions: {
418
- from: ${JSON.stringify(sourceTable)}
419
- to: ${JSON.stringify(targetTable)}
420
- isOneToOne: true
421
- isSetofReturn: false
422
- }`;
423
- }
424
- }
425
- }
426
-
427
- // A ROWS 1 estimate may only collapse the array type when SetofOptions is also emitted
428
- // (returnTableName resolved): postgrest-js relies on isOneToOne to restore the array shape
429
- // for direct RPC calls. Without that metadata PostgREST still returns an array, so the
430
- // collapse would make the type diverge from the actual response.
431
- const collapsibleSingleRow =
432
- !returnsMultipleRows && returnTableName !== null;
433
- return `${returnType}${fn.is_set_returning_function && !collapsibleSingleRow ? "[]" : ""}
434
- ${setofOptionsInfo ? `${setofOptionsInfo}` : ""}`;
323
+ const { setof_options } = overload;
324
+ const setofOptionsInfo = setof_options
325
+ ? `SetofOptions: {
326
+ from: ${JSON.stringify(setof_options.from)}
327
+ to: ${JSON.stringify(setof_options.to)}
328
+ isOneToOne: ${setof_options.is_one_to_one}
329
+ isSetofReturn: ${setof_options.is_setof_return}
330
+ }`
331
+ : "";
332
+ // A ROWS 1 estimate may only collapse the array type when SetofOptions is also emitted:
333
+ // postgrest-js relies on isOneToOne to restore the array shape for direct RPC calls.
334
+ // Without that metadata PostgREST still returns an array, so the collapse would make
335
+ // the type diverge from the actual response.
336
+ const collapsibleSingleRow = setof_options?.is_one_to_one === true;
337
+ return `${returnType}${overload.is_set_returning && !collapsibleSingleRow ? "[]" : ""}
338
+ ${setofOptionsInfo}`;
435
339
  };
436
340
 
437
341
  const getFunctionReturnType = (
438
342
  schema: PostgresSchema,
439
- fn: PostgresFunction,
343
+ { returns }: RpcFunctionOverload,
440
344
  ): string => {
441
345
  // Case 1: `returns table`.
442
- const tableArgs = fn.args.filter(({ mode }) => mode === "table");
443
- if (tableArgs.length > 0) {
444
- const argsNameAndType = tableArgs.map(({ name, type_id }) => {
445
- const type = typesById.get(type_id);
446
- let tsType = "unknown";
447
- if (type) {
448
- tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
449
- }
450
- return { name, type: tsType };
451
- });
452
-
346
+ if (returns.columns) {
453
347
  return `{
454
- ${argsNameAndType.map(({ name, type }) => `${JSON.stringify(name)}: ${type}`)}
348
+ ${returns.columns.map(
349
+ (column) =>
350
+ `${JSON.stringify(column.name)}: ${referencedTsType(schema, column)}`,
351
+ )}
455
352
  }`;
456
353
  }
457
354
 
458
- // Case 2: returns a relation's row type.
355
+ // Case 2: returns a row type of a relation in this schema.
459
356
  const relation =
460
- introspectionBySchema[schema.name]?.tables.find(
461
- ({ table: { id } }) => id === fn.return_type_relation_id,
462
- )?.table ||
463
- introspectionBySchema[schema.name]?.views.find(
464
- ({ view: { id } }) => id === fn.return_type_relation_id,
465
- )?.view;
466
- if (relation) {
357
+ returns.relation_id === null
358
+ ? undefined
359
+ : relationsById.get(returns.relation_id);
360
+ if (relation && relation.schema === schema.name) {
467
361
  return `{
468
362
  ${getColumns(relation.id)
469
363
  .map((column) =>
@@ -484,195 +378,61 @@ export const generateTypescript = async (
484
378
  }
485
379
 
486
380
  // Case 3: returns base/array/composite/enum type.
487
- const type = typesById.get(fn.return_type_id);
488
- if (type) {
489
- return pgTypeToTsType(schema, type.name, typeContext, type.schema);
490
- }
491
-
492
- return "unknown";
493
- };
494
- // Special error case for functions that take table row but don't qualify as embedded functions
495
- const hasTableRowError = (
496
- fn: PostgresFunction,
497
- inArgs: PostgresFunction["args"],
498
- ) => {
499
- const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
500
- if (
501
- soleInArg &&
502
- soleInArg.name === "" &&
503
- relationTypeByIds.get(soleInArg.type_id) &&
504
- !getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id)
505
- ) {
506
- return true;
507
- }
508
- return false;
381
+ return referencedTsType(schema, returns);
509
382
  };
510
383
 
511
- // Check for generic conflict cases that need error reporting
512
- const getConflictError = (
384
+ const getArgTsType = (
513
385
  schema: PostgresSchema,
514
- fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction["args"] }>,
515
- fn: PostgresFunction,
516
- inArgs: PostgresFunction["args"],
386
+ arg: RpcFunctionOverload["args"][number],
517
387
  ) => {
518
- // If there is a single function definition, there is no conflict
519
- if (fns.length <= 1) return null;
520
- const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
521
-
522
- // Generic conflict detection patterns
523
- // Pattern 1: No-args vs default-args conflicts
524
- if (inArgs.length === 0) {
525
- const conflictingFns = fns.filter(
526
- ({ fn: otherFn, inArgs: otherInArgs }) => {
527
- if (otherFn === fn) return false;
528
- const otherSoleInArg =
529
- otherInArgs.length === 1 ? otherInArgs[0] : undefined;
530
- return (
531
- otherSoleInArg &&
532
- otherSoleInArg.name === "" &&
533
- otherSoleInArg.has_default
534
- );
535
- },
536
- );
537
-
538
- const conflictingFn = conflictingFns[0];
539
- if (conflictingFn) {
540
- const returnTypeName =
541
- typesById.get(conflictingFn.fn.return_type_id)?.name || "unknown";
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`;
543
- }
388
+ const tsType = referencedTsType(schema, arg);
389
+ // A row-typed parameter takes the relation's columns: PostgREST fills the
390
+ // keys it does not receive with NULL, and the computed fields are never
391
+ // input. A named parameter is typed without them. An unnamed one keeps the
392
+ // plain `Row` reference, which is how postgrest-js versions predating
393
+ // `ComputedFields` recognise the function as a computed field.
394
+ if (arg.name === "" || arg.format === null || arg.type_schema === null) {
395
+ return tsType;
544
396
  }
545
-
546
- // Pattern 2: Same parameter name but different types (unresolvable overloads)
547
- if (soleInArg && soleInArg.name !== "") {
548
- const conflictingFns = fns.filter(
549
- ({ fn: otherFn, inArgs: otherInArgs }) => {
550
- if (otherFn === fn) return false;
551
- const otherSoleInArg =
552
- otherInArgs.length === 1 ? otherInArgs[0] : undefined;
553
- return (
554
- otherSoleInArg &&
555
- otherSoleInArg.name === soleInArg.name &&
556
- otherSoleInArg.type_id !== soleInArg.type_id
557
- );
558
- },
559
- );
560
-
561
- if (conflictingFns.length > 0) {
562
- const allConflictingFunctions = [{ fn, inArgs }, ...conflictingFns];
563
- const conflictList = allConflictingFunctions
564
- .sort((a, b) => {
565
- const aArgs = a.inArgs;
566
- const bArgs = b.inArgs;
567
- return (aArgs[0]?.type_id || 0) - (bArgs[0]?.type_id || 0);
568
- })
569
- .map((f) => {
570
- const args = f.inArgs;
571
- return `${schema.name}.${fn.name}(${args
572
- .map(
573
- (a) =>
574
- `${a.name || ""} => ${typesById.get(a.type_id)?.name || "unknown"}`,
575
- )
576
- .join(", ")})`;
577
- })
578
- .join(", ");
579
-
580
- return `Could not choose the best candidate function between: ${conflictList}. Try renaming the parameters or the function itself in the database so function overloading can be resolved`;
581
- }
397
+ const relationReference = findRelationReference(
398
+ arg.format,
399
+ typeContext,
400
+ arg.type_schema,
401
+ );
402
+ if (relationReference && tsType === `${relationReference}['Row']`) {
403
+ return `Omit<${tsType}, ${relationReference}['ComputedFields']>`;
582
404
  }
583
-
584
- return null;
405
+ return tsType;
585
406
  };
586
407
 
587
- const getFunctionSignatures = (
408
+ const getArgsTsType = (
588
409
  schema: PostgresSchema,
589
- fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction["args"] }>,
590
- ) => {
591
- return fns
592
- .map(({ fn, inArgs }) => {
410
+ args: RpcFunctionOverload["args"],
411
+ ) =>
412
+ `{ ${args.map(
413
+ (arg) =>
414
+ `${JSON.stringify(arg.name)}${arg.has_default ? "?" : ""}: ${getArgTsType(schema, arg)}`,
415
+ )} }`;
416
+
417
+ const getFunctionSignatures = (schema: PostgresSchema, fn: RpcFunction) => {
418
+ return fn.overloads
419
+ .map((overload) => {
593
420
  // `never` would be wrong here: postgrest-js treats `Args extends { '': Row }`
594
421
  // as a computed field (`never extends T` is always true, so same-named table
595
422
  // columns get omitted from select results), and an object type with a required
596
423
  // `never` property is uninhabited for any tool doing sound type math on the
597
424
  // generated types. `Record<PropertyKey, never>` is the accurate type for
598
425
  // "callable with no arguments".
599
- let argsType = "Record<PropertyKey, never>";
600
- let returnType = getFunctionReturnType(schema, fn);
601
-
602
- // Check for specific error cases
603
- const conflictError = getConflictError(schema, fns, fn, inArgs);
604
- if (conflictError) {
605
- if (inArgs.length > 0) {
606
- const argsNameAndType = inArgs.map(
607
- ({ name, type_id, has_default }) => {
608
- const type = typesById.get(type_id);
609
- let tsType = "unknown";
610
- if (type) {
611
- tsType = pgTypeToTsType(
612
- schema,
613
- type.name,
614
- typeContext,
615
- type.schema,
616
- );
617
- }
618
- return { name, type: tsType, has_default };
619
- },
620
- );
621
- argsType = `{ ${argsNameAndType.map(
622
- ({ name, type, has_default }) =>
623
- `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`,
624
- )} }`;
625
- }
626
- returnType = `{ error: true } & ${JSON.stringify(conflictError)}`;
627
- } else if (hasTableRowError(fn, inArgs)) {
628
- // Special case for computed fields returning scalars functions
629
- if (inArgs.length > 0) {
630
- const argsNameAndType = inArgs.map(
631
- ({ name, type_id, has_default }) => {
632
- const type = typesById.get(type_id);
633
- let tsType = "unknown";
634
- if (type) {
635
- tsType = pgTypeToTsType(
636
- schema,
637
- type.name,
638
- typeContext,
639
- type.schema,
640
- );
641
- }
642
- return { name, type: tsType, has_default };
643
- },
644
- );
645
- argsType = `{ ${argsNameAndType.map(
646
- ({ name, type, has_default }) =>
647
- `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`,
648
- )} }`;
649
- }
650
- returnType = `{ error: true } & ${JSON.stringify(
651
- `the function ${schema.name}.${fn.name} with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache`,
652
- )}`;
653
- } else if (inArgs.length > 0) {
654
- const argsNameAndType = inArgs.map(
655
- ({ name, type_id, has_default }) => {
656
- const type = typesById.get(type_id);
657
- let tsType = "unknown";
658
- if (type) {
659
- tsType = pgTypeToTsType(
660
- schema,
661
- type.name,
662
- typeContext,
663
- type.schema,
664
- );
665
- }
666
- return { name, type: tsType, has_default };
667
- },
668
- );
669
- argsType = `{ ${argsNameAndType.map(
670
- ({ name, type, has_default }) =>
671
- `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`,
672
- )} }`;
673
- }
674
-
675
- return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }`;
426
+ const argsType =
427
+ overload.args.length > 0
428
+ ? getArgsTsType(schema, overload.args)
429
+ : "Record<PropertyKey, never>";
430
+ const returnType =
431
+ overload.error === null
432
+ ? getFunctionReturnType(schema, overload)
433
+ : `{ error: true } & ${JSON.stringify(overload.error)}`;
434
+
435
+ return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(overload, returnType)} }`;
676
436
  })
677
437
  .join(" |\n");
678
438
  };
@@ -716,42 +476,37 @@ export const generateTypescript = async (
716
476
  }
717
477
 
718
478
  /**
719
- * Is `fn` a PostgREST computed field on `relation` (a table or view)?
720
- *
721
- * A computed field is a function taking a single argument of the relation's
722
- * own composite type, so it is selectable as if it were a column and belongs
723
- * in `Row`.
724
- *
725
- * Matching on the argument's type OID rather than on `argument_types` is what
726
- * makes named parameters work. `argument_types` is
727
- * `pg_get_function_arguments()`, which renders the parameter *name* when the
728
- * parameter has one — `category category` rather than `category` — and
729
- * schema-qualifies the type when it is not visible in `search_path`. Both
730
- * forms failed a bare string comparison against the relation name, so
731
- * `create function name_translated(category category)` silently lost its
732
- * computed field while the unnamed `name_translated(category)` kept it.
733
- * See supabase/postgres-meta#1034.
479
+ * The computed fields of a relation, in function name order. A computed
480
+ * field is a function taking a single argument of the relation's own
481
+ * composite type, so it is selectable as if it were a column and belongs in
482
+ * `Row`; `resolveRpcFunctions` marks them with `computed_field_of`.
734
483
  */
735
- const isComputedFieldOf = (
736
- fn: PostgresFunction,
737
- inArgs: PostgresFunction["args"],
738
- relation: { id: number; name: string },
739
- ) => {
740
- const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
741
- if (soleInArg) {
742
- // `relationTypeByIds` is already "types backed by a relation", so a hit
743
- // identifies the relation regardless of how the argument was spelled.
744
- // A `variadic` argument is an array type and never appears here.
745
- const argType = relationTypeByIds.get(soleInArg.type_id);
746
- if (argType) {
747
- return argType.type_relation_id === relation.id;
748
- }
749
- }
750
- // Fall back to the historical comparison when the argument's type is not in
751
- // the metadata (callers may pass a reduced `types` list), so a computed
752
- // field that resolved before keeps resolving.
753
- return fn.argument_types === relation.name;
754
- };
484
+ const computedFieldsOf = (
485
+ schemaFunctions: RpcFunction[],
486
+ relation: { id: number },
487
+ ) =>
488
+ schemaFunctions.flatMap((fn) => {
489
+ const overload = fn.overloads.find(
490
+ ({ computed_field_of }) => computed_field_of === relation.id,
491
+ );
492
+ return overload ? [{ name: fn.name, overload }] : [];
493
+ });
494
+
495
+ const computedFieldTsDefinition = (
496
+ schema: PostgresSchema,
497
+ { name, overload }: { name: string; overload: RpcFunctionOverload },
498
+ ) =>
499
+ `${JSON.stringify(name)}: ${generateNullableUnionTsType(
500
+ getFunctionReturnType(schema, overload),
501
+ true,
502
+ )}`;
503
+
504
+ // `ComputedFields` names the `Row` keys PostgREST leaves out of `select('*')`,
505
+ // so postgrest-js does not have to infer them from `Functions`.
506
+ const computedFieldsTsType = (computedFields: { name: string }[]) =>
507
+ computedFields.length === 0
508
+ ? "never"
509
+ : computedFields.map(({ name }) => JSON.stringify(name)).join(" | ");
755
510
 
756
511
  let output = `
757
512
  export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
@@ -771,11 +526,12 @@ export type Database = {
771
526
  ${
772
527
  schemaTables.length === 0
773
528
  ? "[_ in never]: never"
774
- : schemaTables.map(
775
- ({
529
+ : schemaTables.map(({ table, relationships }) => {
530
+ const computedFields = computedFieldsOf(
531
+ schemaFunctions,
776
532
  table,
777
- relationships,
778
- }) => `${JSON.stringify(table.name)}: {
533
+ );
534
+ return `${JSON.stringify(table.name)}: {
779
535
  Row: {
780
536
  ${[
781
537
  ...getColumns(table.id).map((column) =>
@@ -791,18 +547,12 @@ export type Database = {
791
547
  typeContext,
792
548
  ),
793
549
  ),
794
- ...schemaFunctions
795
- .filter(({ fn, inArgs }) =>
796
- isComputedFieldOf(fn, inArgs, table),
797
- )
798
- .map(({ fn }) => {
799
- return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
800
- getFunctionReturnType(schema, fn),
801
- true,
802
- )}`;
803
- }),
550
+ ...computedFields.map((computedField) =>
551
+ computedFieldTsDefinition(schema, computedField),
552
+ ),
804
553
  ]}
805
554
  }
555
+ ComputedFields: ${computedFieldsTsType(computedFields)}
806
556
  Insert: {
807
557
  ${getColumns(table.id).map((column) => {
808
558
  if (
@@ -852,19 +602,20 @@ export type Database = {
852
602
  Relationships: [
853
603
  ${relationships.map(generateRelationshiptTsDefinition)}
854
604
  ]
855
- }`,
856
- )
605
+ }`;
606
+ })
857
607
  }
858
608
  }
859
609
  Views: {
860
610
  ${
861
611
  schemaViews.length === 0
862
612
  ? "[_ in never]: never"
863
- : schemaViews.map(
864
- ({
613
+ : schemaViews.map(({ view, relationships }) => {
614
+ const computedFields = computedFieldsOf(
615
+ schemaFunctions,
865
616
  view,
866
- relationships,
867
- }) => `${JSON.stringify(view.name)}: {
617
+ );
618
+ return `${JSON.stringify(view.name)}: {
868
619
  Row: {
869
620
  ${[
870
621
  ...getColumns(view.id).map((column) =>
@@ -880,19 +631,12 @@ export type Database = {
880
631
  typeContext,
881
632
  ),
882
633
  ),
883
- ...schemaFunctions
884
- .filter(({ fn, inArgs }) =>
885
- isComputedFieldOf(fn, inArgs, view),
886
- )
887
- .map(
888
- ({ fn }) =>
889
- `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
890
- getFunctionReturnType(schema, fn),
891
- true,
892
- )}`,
893
- ),
634
+ ...computedFields.map((computedField) =>
635
+ computedFieldTsDefinition(schema, computedField),
636
+ ),
894
637
  ]}
895
638
  }
639
+ ComputedFields: ${computedFieldsTsType(computedFields)}
896
640
  ${
897
641
  // Metadata predating the trigger-aware flags (still valid
898
642
  // version 1 documents) falls back to the old gate.
@@ -942,37 +686,21 @@ export type Database = {
942
686
  }Relationships: [
943
687
  ${relationships.map(generateRelationshiptTsDefinition)}
944
688
  ]
945
- }`,
946
- )
689
+ }`;
690
+ })
947
691
  }
948
692
  }
949
693
  Functions: {
950
- ${(() => {
951
- if (schemaFunctions.length === 0) {
952
- return "[_ in never]: never";
953
- }
954
- const schemaFunctionsGroupedByName = schemaFunctions.reduce(
955
- (acc, curr) => {
956
- (acc[curr.fn.name] ??= []).push(curr);
957
- return acc;
958
- },
959
- {} as Record<string, typeof schemaFunctions>,
960
- );
961
- for (const fns of Object.values(schemaFunctionsGroupedByName)) {
962
- fns.sort(
963
- (a, b) =>
964
- compareStrings(a.fn.argument_types, b.fn.argument_types) ||
965
- compareStrings(a.fn.return_type, b.fn.return_type),
966
- );
967
- }
968
-
969
- return Object.entries(schemaFunctionsGroupedByName)
970
- .map(([fnName, fns]) => {
971
- const functionSignatures = getFunctionSignatures(schema, fns);
972
- return `${JSON.stringify(fnName)}:\n${functionSignatures}`;
973
- })
974
- .join(",\n");
975
- })()}
694
+ ${
695
+ schemaFunctions.length === 0
696
+ ? "[_ in never]: never"
697
+ : schemaFunctions
698
+ .map(
699
+ (fn) =>
700
+ `${JSON.stringify(fn.name)}:\n${getFunctionSignatures(schema, fn)}`,
701
+ )
702
+ .join(",\n")
703
+ }
976
704
  }
977
705
  Enums: {
978
706
  ${
@@ -1020,7 +748,7 @@ export type Database = {
1020
748
  type DatabaseWithoutInternals = Omit<Database, '__InternalSupabase'>
1021
749
 
1022
750
  type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, ${JSON.stringify(
1023
- defaultSchema,
751
+ resolvedDefaultSchema,
1024
752
  )}>]
1025
753
 
1026
754
  export type Tables<
@@ -1157,14 +885,7 @@ export const pgTypeToTsType = (
1157
885
  // it whenever the owning schema is known.
1158
886
  typeSchema?: string,
1159
887
  ): string => {
1160
- const {
1161
- types,
1162
- schemas,
1163
- tables,
1164
- views,
1165
- foreignTables = [],
1166
- materializedViews = [],
1167
- } = context;
888
+ const { types, schemas } = context;
1168
889
  if (pgType === "bool") {
1169
890
  return "boolean";
1170
891
  } else if (
@@ -1228,42 +949,63 @@ export const pgTypeToTsType = (
1228
949
  return "unknown";
1229
950
  }
1230
951
 
1231
- // A relation-typed value carries only a bare type name, so several relations
1232
- // can share it and `preferredSchema` is what disambiguates them. Match on
1233
- // the schema across every relation kind before falling back to kind order,
1234
- // otherwise a relation in an unrelated schema outranks the one the caller
1235
- // meant. Foreign tables are generated under `Tables` and materialized views
1236
- // under `Views`, alongside their plain counterparts.
1237
- const relationKinds = [
1238
- ["Tables", tables],
1239
- ["Tables", foreignTables],
1240
- ["Views", views],
1241
- ["Views", materializedViews],
1242
- ] as const;
1243
- const findRelation = (
1244
- matches: (relation: { name: string; schema: string }) => boolean,
1245
- ) => {
1246
- for (const [kind, relations] of relationKinds) {
1247
- const relation = relations.find(matches);
1248
- if (relation) return { kind, relation };
1249
- }
1250
- return undefined;
1251
- };
1252
- const match =
1253
- findRelation(
1254
- ({ name, schema: relationSchema }) =>
1255
- name === pgType && relationSchema === preferredSchema,
1256
- ) ?? findRelation(({ name }) => name === pgType);
1257
- if (match) {
1258
- const { kind, relation } = match;
1259
- if (schemas.some(({ name }) => name === relation.schema)) {
1260
- return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(
1261
- relation.name,
1262
- )}]['Row']`;
1263
- }
1264
- return "unknown";
1265
- }
952
+ const relationReference = findRelationReference(
953
+ pgType,
954
+ context,
955
+ preferredSchema,
956
+ );
957
+ return relationReference ? `${relationReference}['Row']` : "unknown";
958
+ }
959
+ };
1266
960
 
1267
- return "unknown";
961
+ /**
962
+ * The `Database[schema][kind][name]` reference of the relation whose row type
963
+ * is `pgType`, or `undefined` when no generated relation has that type.
964
+ *
965
+ * A relation-typed value carries only a bare type name, so several relations
966
+ * can share it and `preferredSchema` is what disambiguates them. Match on the
967
+ * schema across every relation kind before falling back to kind order,
968
+ * otherwise a relation in an unrelated schema outranks the one the caller
969
+ * meant. Foreign tables are generated under `Tables` and materialized views
970
+ * under `Views`, alongside their plain counterparts.
971
+ */
972
+ const findRelationReference = (
973
+ pgType: string,
974
+ context: TypeResolutionContext,
975
+ preferredSchema: string,
976
+ ): string | undefined => {
977
+ const {
978
+ schemas,
979
+ tables,
980
+ views,
981
+ foreignTables = [],
982
+ materializedViews = [],
983
+ } = context;
984
+ const relationKinds = [
985
+ ["Tables", tables],
986
+ ["Tables", foreignTables],
987
+ ["Views", views],
988
+ ["Views", materializedViews],
989
+ ] as const;
990
+ const findRelation = (
991
+ matches: (relation: { name: string; schema: string }) => boolean,
992
+ ) => {
993
+ for (const [kind, relations] of relationKinds) {
994
+ const relation = relations.find(matches);
995
+ if (relation) return { kind, relation };
996
+ }
997
+ return undefined;
998
+ };
999
+ const match =
1000
+ findRelation(
1001
+ ({ name, schema: relationSchema }) =>
1002
+ name === pgType && relationSchema === preferredSchema,
1003
+ ) ?? findRelation(({ name }) => name === pgType);
1004
+ if (!match || !schemas.some(({ name }) => name === match.relation.schema)) {
1005
+ return undefined;
1268
1006
  }
1007
+ const { kind, relation } = match;
1008
+ return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(
1009
+ relation.name,
1010
+ )}]`;
1269
1011
  };