@supabase/postgrest-typegen 0.3.0 → 0.4.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
@@ -127,6 +127,9 @@ import {
127
127
  | `generatePython` | — |
128
128
  | `generateSwift` | `{ accessControl?: 'internal' \| 'public' \| 'private' \| 'package' }` |
129
129
 
130
+ Every generator returns the complete contents of the generated file, ending
131
+ with a single newline, so consumers write the result as-is.
132
+
130
133
  `generateTypescript` formats its output with `oxfmt` unless `format` is
131
134
  supplied. `oxfmt` is an optional peer dependency, imported only when that
132
135
  default runs: install it to use the default formatter, or pass your own
@@ -148,7 +151,7 @@ npm install oxfmt@0.66.0
148
151
 
149
152
  ## Releasing
150
153
 
151
- 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.
154
+ 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`.
152
155
 
153
156
  ## License
154
157
 
@@ -8,8 +8,7 @@ export const generateGo = ({ schemas, tables, views, materializedViews, columns,
8
8
  return acc;
9
9
  }, {});
10
10
  const compositeTypes = types.filter((type) => type.attributes.length > 0);
11
- let output = `
12
- package database
11
+ let output = `package database
13
12
 
14
13
  ${tables
15
14
  .filter((table) => schemas.some((schema) => schema.name === table.schema))
@@ -30,8 +29,8 @@ ${compositeTypes
30
29
  .filter((compositeType) => schemas.some((schema) => schema.name === compositeType.schema))
31
30
  .map((compositeType) => generateCompositeTypeStruct(schemas.find((schema) => schema.name === compositeType.schema), compositeType, types))
32
31
  .join("\n\n")}
33
- `.trim();
34
- return output;
32
+ `.trimEnd();
33
+ return `${output}\n`;
35
34
  };
36
35
  /**
37
36
  * Converts a Postgres name to PascalCase.
@@ -16,8 +16,7 @@ export const generatePython = ({ schemas, tables, views, materializedViews, colu
16
16
  .map((type) => ctx.typeToClass(type));
17
17
  const py_views = views.map((view) => ctx.viewToClass(view));
18
18
  const py_matviews = materializedViews.map((matview) => ctx.matViewToClass(matview));
19
- let output = `
20
- from __future__ import annotations
19
+ let output = `from __future__ import annotations
21
20
 
22
21
  import datetime
23
22
  import uuid
@@ -43,8 +42,8 @@ ${concatLines(py_matviews)}
43
42
 
44
43
  ${concatLines(composite_types)}
45
44
 
46
- `.trim();
47
- return output;
45
+ `.trimEnd();
46
+ return `${output}\n`;
48
47
  };
49
48
  class PythonContext {
50
49
  types;
@@ -216,7 +216,7 @@ export const generateSwift = (metadata, opts = {}) => {
216
216
  ];
217
217
  }),
218
218
  ];
219
- return output.join("\n");
219
+ return `${output.join("\n")}\n`;
220
220
  };
221
221
  // TODO: Make this more robust. Currently doesn't handle range types - returns them as string.
222
222
  const pgTypeToSwiftType = (pgType, nullable, { types, views, tables, }) => {
@@ -359,6 +359,27 @@ export const generateTypescript = async (metadata, opts = {}) => {
359
359
  }
360
360
  return null;
361
361
  };
362
+ const getArgTsType = (schema, { name, type_id }) => {
363
+ const type = typesById.get(type_id);
364
+ if (!type) {
365
+ return "unknown";
366
+ }
367
+ const tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
368
+ // A row-typed parameter takes the relation's columns: PostgREST fills the
369
+ // keys it does not receive with NULL, and the computed fields are never
370
+ // input. A named parameter is typed without them. An unnamed one keeps the
371
+ // plain `Row` reference, which is how postgrest-js versions predating
372
+ // `ComputedFields` recognise the function as a computed field.
373
+ if (name === "") {
374
+ return tsType;
375
+ }
376
+ const relationReference = findRelationReference(type.name, typeContext, type.schema);
377
+ if (relationReference && tsType === `${relationReference}['Row']`) {
378
+ return `Omit<${tsType}, ${relationReference}['ComputedFields']>`;
379
+ }
380
+ return tsType;
381
+ };
382
+ const getArgsTsType = (schema, inArgs) => `{ ${inArgs.map((arg) => `${JSON.stringify(arg.name)}${arg.has_default ? "?" : ""}: ${getArgTsType(schema, arg)}`)} }`;
362
383
  const getFunctionSignatures = (schema, fns) => {
363
384
  return fns
364
385
  .map(({ fn, inArgs }) => {
@@ -368,50 +389,19 @@ export const generateTypescript = async (metadata, opts = {}) => {
368
389
  // `never` property is uninhabited for any tool doing sound type math on the
369
390
  // generated types. `Record<PropertyKey, never>` is the accurate type for
370
391
  // "callable with no arguments".
371
- let argsType = "Record<PropertyKey, never>";
392
+ const argsType = inArgs.length > 0
393
+ ? getArgsTsType(schema, inArgs)
394
+ : "Record<PropertyKey, never>";
372
395
  let returnType = getFunctionReturnType(schema, fn);
373
396
  // Check for specific error cases
374
397
  const conflictError = getConflictError(schema, fns, fn, inArgs);
375
398
  if (conflictError) {
376
- if (inArgs.length > 0) {
377
- const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
378
- const type = typesById.get(type_id);
379
- let tsType = "unknown";
380
- if (type) {
381
- tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
382
- }
383
- return { name, type: tsType, has_default };
384
- });
385
- argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`)} }`;
386
- }
387
399
  returnType = `{ error: true } & ${JSON.stringify(conflictError)}`;
388
400
  }
389
401
  else if (hasTableRowError(fn, inArgs)) {
390
402
  // Special case for computed fields returning scalars functions
391
- if (inArgs.length > 0) {
392
- const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
393
- const type = typesById.get(type_id);
394
- let tsType = "unknown";
395
- if (type) {
396
- tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
397
- }
398
- return { name, type: tsType, has_default };
399
- });
400
- argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`)} }`;
401
- }
402
403
  returnType = `{ error: true } & ${JSON.stringify(`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`)}`;
403
404
  }
404
- else if (inArgs.length > 0) {
405
- const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
406
- const type = typesById.get(type_id);
407
- let tsType = "unknown";
408
- if (type) {
409
- tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
410
- }
411
- return { name, type: tsType, has_default };
412
- });
413
- argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`)} }`;
414
- }
415
405
  return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }`;
416
406
  })
417
407
  .join(" |\n");
@@ -471,6 +461,30 @@ export const generateTypescript = async (metadata, opts = {}) => {
471
461
  // field that resolved before keeps resolving.
472
462
  return fn.argument_types === relation.name;
473
463
  };
464
+ /**
465
+ * The computed fields of a relation, in `schemaFunctions` order. A function
466
+ * named like one of the relation's columns is left out: PostgREST resolves
467
+ * the name to the column, so it is neither selectable nor part of `Row`.
468
+ */
469
+ const computedFieldsOf = (schemaFunctions, relation) => {
470
+ const columnNames = new Set(getColumns(relation.id).map((column) => column.name));
471
+ const seen = new Set();
472
+ return schemaFunctions.filter(({ fn, inArgs }) => {
473
+ if (!isComputedFieldOf(fn, inArgs, relation) ||
474
+ columnNames.has(fn.name) ||
475
+ seen.has(fn.name)) {
476
+ return false;
477
+ }
478
+ seen.add(fn.name);
479
+ return true;
480
+ });
481
+ };
482
+ const computedFieldTsDefinition = (schema, { fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`;
483
+ // `ComputedFields` names the `Row` keys PostgREST leaves out of `select('*')`,
484
+ // so postgrest-js does not have to infer them from `Functions`.
485
+ const computedFieldsTsType = (computedFields) => computedFields.length === 0
486
+ ? "never"
487
+ : computedFields.map(({ fn }) => JSON.stringify(fn.name)).join(" | ");
474
488
  let output = `
475
489
  export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
476
490
 
@@ -482,117 +496,119 @@ export type Database = {
482
496
  Tables: {
483
497
  ${schemaTables.length === 0
484
498
  ? "[_ in never]: never"
485
- : schemaTables.map(({ table, relationships, }) => `${JSON.stringify(table.name)}: {
499
+ : schemaTables.map(({ table, relationships }) => {
500
+ const computedFields = computedFieldsOf(schemaFunctions, table);
501
+ return `${JSON.stringify(table.name)}: {
486
502
  Row: {
487
503
  ${[
488
- ...getColumns(table.id).map((column) => generateColumnTsDefinition(schema, {
489
- name: column.name,
490
- format: column.format,
491
- type_schema: column.type_schema,
492
- is_nullable: column.is_nullable,
493
- is_optional: false,
494
- }, typeContext)),
495
- ...schemaFunctions
496
- .filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, table))
497
- .map(({ fn }) => {
498
- return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`;
499
- }),
500
- ]}
504
+ ...getColumns(table.id).map((column) => generateColumnTsDefinition(schema, {
505
+ name: column.name,
506
+ format: column.format,
507
+ type_schema: column.type_schema,
508
+ is_nullable: column.is_nullable,
509
+ is_optional: false,
510
+ }, typeContext)),
511
+ ...computedFields.map((computedField) => computedFieldTsDefinition(schema, computedField)),
512
+ ]}
501
513
  }
514
+ ComputedFields: ${computedFieldsTsType(computedFields)}
502
515
  Insert: {
503
516
  ${getColumns(table.id).map((column) => {
504
- if (column.identity_generation === "ALWAYS" ||
505
- column.is_generated) {
506
- return `${JSON.stringify(column.name)}?: never`;
507
- }
508
- return generateColumnTsDefinition(schema, {
509
- name: column.name,
510
- format: column.format,
511
- type_schema: column.type_schema,
512
- is_nullable: column.is_nullable,
513
- is_optional: column.is_nullable ||
514
- column.is_identity ||
515
- column.default_value !== null,
516
- }, typeContext);
517
- })}
517
+ if (column.identity_generation === "ALWAYS" ||
518
+ column.is_generated) {
519
+ return `${JSON.stringify(column.name)}?: never`;
520
+ }
521
+ return generateColumnTsDefinition(schema, {
522
+ name: column.name,
523
+ format: column.format,
524
+ type_schema: column.type_schema,
525
+ is_nullable: column.is_nullable,
526
+ is_optional: column.is_nullable ||
527
+ column.is_identity ||
528
+ column.default_value !== null,
529
+ }, typeContext);
530
+ })}
518
531
  }
519
532
  Update: {
520
533
  ${getColumns(table.id).map((column) => {
521
- if (column.identity_generation === "ALWAYS" ||
522
- column.is_generated) {
523
- return `${JSON.stringify(column.name)}?: never`;
524
- }
525
- return generateColumnTsDefinition(schema, {
526
- name: column.name,
527
- format: column.format,
528
- type_schema: column.type_schema,
529
- is_nullable: column.is_nullable,
530
- is_optional: true,
531
- }, typeContext);
532
- })}
534
+ if (column.identity_generation === "ALWAYS" ||
535
+ column.is_generated) {
536
+ return `${JSON.stringify(column.name)}?: never`;
537
+ }
538
+ return generateColumnTsDefinition(schema, {
539
+ name: column.name,
540
+ format: column.format,
541
+ type_schema: column.type_schema,
542
+ is_nullable: column.is_nullable,
543
+ is_optional: true,
544
+ }, typeContext);
545
+ })}
533
546
  }
534
547
  Relationships: [
535
548
  ${relationships.map(generateRelationshiptTsDefinition)}
536
549
  ]
537
- }`)}
550
+ }`;
551
+ })}
538
552
  }
539
553
  Views: {
540
554
  ${schemaViews.length === 0
541
555
  ? "[_ in never]: never"
542
- : schemaViews.map(({ view, relationships, }) => `${JSON.stringify(view.name)}: {
556
+ : schemaViews.map(({ view, relationships }) => {
557
+ const computedFields = computedFieldsOf(schemaFunctions, view);
558
+ return `${JSON.stringify(view.name)}: {
543
559
  Row: {
544
560
  ${[
545
- ...getColumns(view.id).map((column) => generateColumnTsDefinition(schema, {
546
- name: column.name,
547
- format: column.format,
548
- type_schema: column.type_schema,
549
- is_nullable: column.is_nullable,
550
- is_optional: false,
551
- }, typeContext)),
552
- ...schemaFunctions
553
- .filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, view))
554
- .map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
555
- ]}
556
- }
557
- ${
558
- // Metadata predating the trigger-aware flags (still valid
559
- // version 1 documents) falls back to the old gate.
560
- (view.is_insert_enabled ?? view.is_updatable)
561
- ? `Insert: {
562
- ${getColumns(view.id).map((column) => {
563
- if (!column.is_updatable) {
564
- return `${JSON.stringify(column.name)}?: never`;
565
- }
566
- return generateColumnTsDefinition(schema, {
561
+ ...getColumns(view.id).map((column) => generateColumnTsDefinition(schema, {
567
562
  name: column.name,
568
563
  format: column.format,
569
564
  type_schema: column.type_schema,
570
- is_nullable: true,
571
- is_optional: true,
572
- }, typeContext);
573
- })}
565
+ is_nullable: column.is_nullable,
566
+ is_optional: false,
567
+ }, typeContext)),
568
+ ...computedFields.map((computedField) => computedFieldTsDefinition(schema, computedField)),
569
+ ]}
570
+ }
571
+ ComputedFields: ${computedFieldsTsType(computedFields)}
572
+ ${
573
+ // Metadata predating the trigger-aware flags (still valid
574
+ // version 1 documents) falls back to the old gate.
575
+ (view.is_insert_enabled ?? view.is_updatable)
576
+ ? `Insert: {
577
+ ${getColumns(view.id).map((column) => {
578
+ if (!column.is_updatable) {
579
+ return `${JSON.stringify(column.name)}?: never`;
580
+ }
581
+ return generateColumnTsDefinition(schema, {
582
+ name: column.name,
583
+ format: column.format,
584
+ type_schema: column.type_schema,
585
+ is_nullable: true,
586
+ is_optional: true,
587
+ }, typeContext);
588
+ })}
574
589
  }
575
590
  `
576
- : ""}${(view.is_update_enabled ?? view.is_updatable)
577
- ? `Update: {
591
+ : ""}${(view.is_update_enabled ?? view.is_updatable)
592
+ ? `Update: {
578
593
  ${getColumns(view.id).map((column) => {
579
- if (!column.is_updatable) {
580
- return `${JSON.stringify(column.name)}?: never`;
581
- }
582
- return generateColumnTsDefinition(schema, {
583
- name: column.name,
584
- format: column.format,
585
- type_schema: column.type_schema,
586
- is_nullable: true,
587
- is_optional: true,
588
- }, typeContext);
589
- })}
594
+ if (!column.is_updatable) {
595
+ return `${JSON.stringify(column.name)}?: never`;
596
+ }
597
+ return generateColumnTsDefinition(schema, {
598
+ name: column.name,
599
+ format: column.format,
600
+ type_schema: column.type_schema,
601
+ is_nullable: true,
602
+ is_optional: true,
603
+ }, typeContext);
604
+ })}
590
605
  }
591
606
  `
592
- : ""}Relationships: [
607
+ : ""}Relationships: [
593
608
  ${relationships.map(generateRelationshiptTsDefinition)}
594
609
  ]
595
- }`)}
610
+ }`;
611
+ })}
596
612
  }
597
613
  Functions: {
598
614
  ${(() => {
@@ -769,7 +785,7 @@ export const pgTypeToTsType = (schema, pgType, context,
769
785
  // schema being generated) when not provided, which is only a guess, so pass
770
786
  // it whenever the owning schema is known.
771
787
  typeSchema) => {
772
- const { types, schemas, tables, views, foreignTables = [], materializedViews = [], } = context;
788
+ const { types, schemas } = context;
773
789
  if (pgType === "bool") {
774
790
  return "boolean";
775
791
  }
@@ -824,34 +840,41 @@ typeSchema) => {
824
840
  }
825
841
  return "unknown";
826
842
  }
827
- // A relation-typed value carries only a bare type name, so several relations
828
- // can share it and `preferredSchema` is what disambiguates them. Match on
829
- // the schema across every relation kind before falling back to kind order,
830
- // otherwise a relation in an unrelated schema outranks the one the caller
831
- // meant. Foreign tables are generated under `Tables` and materialized views
832
- // under `Views`, alongside their plain counterparts.
833
- const relationKinds = [
834
- ["Tables", tables],
835
- ["Tables", foreignTables],
836
- ["Views", views],
837
- ["Views", materializedViews],
838
- ];
839
- const findRelation = (matches) => {
840
- for (const [kind, relations] of relationKinds) {
841
- const relation = relations.find(matches);
842
- if (relation)
843
- return { kind, relation };
844
- }
845
- return undefined;
846
- };
847
- const match = findRelation(({ name, schema: relationSchema }) => name === pgType && relationSchema === preferredSchema) ?? findRelation(({ name }) => name === pgType);
848
- if (match) {
849
- const { kind, relation } = match;
850
- if (schemas.some(({ name }) => name === relation.schema)) {
851
- return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(relation.name)}]['Row']`;
852
- }
853
- return "unknown";
843
+ const relationReference = findRelationReference(pgType, context, preferredSchema);
844
+ return relationReference ? `${relationReference}['Row']` : "unknown";
845
+ }
846
+ };
847
+ /**
848
+ * The `Database[schema][kind][name]` reference of the relation whose row type
849
+ * is `pgType`, or `undefined` when no generated relation has that type.
850
+ *
851
+ * A relation-typed value carries only a bare type name, so several relations
852
+ * can share it and `preferredSchema` is what disambiguates them. Match on the
853
+ * schema across every relation kind before falling back to kind order,
854
+ * otherwise a relation in an unrelated schema outranks the one the caller
855
+ * meant. Foreign tables are generated under `Tables` and materialized views
856
+ * under `Views`, alongside their plain counterparts.
857
+ */
858
+ const findRelationReference = (pgType, context, preferredSchema) => {
859
+ const { schemas, tables, views, foreignTables = [], materializedViews = [], } = context;
860
+ const relationKinds = [
861
+ ["Tables", tables],
862
+ ["Tables", foreignTables],
863
+ ["Views", views],
864
+ ["Views", materializedViews],
865
+ ];
866
+ const findRelation = (matches) => {
867
+ for (const [kind, relations] of relationKinds) {
868
+ const relation = relations.find(matches);
869
+ if (relation)
870
+ return { kind, relation };
854
871
  }
855
- return "unknown";
872
+ return undefined;
873
+ };
874
+ const match = findRelation(({ name, schema: relationSchema }) => name === pgType && relationSchema === preferredSchema) ?? findRelation(({ name }) => name === pgType);
875
+ if (!match || !schemas.some(({ name }) => name === match.relation.schema)) {
876
+ return undefined;
856
877
  }
878
+ const { kind, relation } = match;
879
+ return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(relation.name)}]`;
857
880
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/postgrest-typegen",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Type generation for PostgREST from PostgreSQL schemas",
5
5
  "keywords": [
6
6
  "codegen",
@@ -62,7 +62,7 @@
62
62
  "test": "bun scripts/run-tests.ts test/"
63
63
  },
64
64
  "dependencies": {
65
- "arktype": "2.2.3"
65
+ "arktype": "2.2.5"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@testcontainers/postgresql": "^12.1.0",
@@ -74,8 +74,8 @@
74
74
  "@types/pg": "^8.23.1",
75
75
  "knip": "^6.32.3",
76
76
  "oxfmt": "0.66.0",
77
- "oxlint": "1.83.0",
78
- "oxlint-tsgolint": "7.0.2002",
77
+ "oxlint": "1.85.0",
78
+ "oxlint-tsgolint": "7.0.2003",
79
79
  "pg": "^8.23.0",
80
80
  "testcontainers": "^12.1.0",
81
81
  "typescript": "7.0.2"
@@ -32,8 +32,7 @@ export const generateGo = ({
32
32
 
33
33
  const compositeTypes = types.filter((type) => type.attributes.length > 0);
34
34
 
35
- let output = `
36
- package database
35
+ let output = `package database
37
36
 
38
37
  ${tables
39
38
  .filter((table) => schemas.some((schema) => schema.name === table.schema))
@@ -88,9 +87,9 @@ ${compositeTypes
88
87
  ),
89
88
  )
90
89
  .join("\n\n")}
91
- `.trim();
90
+ `.trimEnd();
92
91
 
93
- return output;
92
+ return `${output}\n`;
94
93
  };
95
94
 
96
95
  /**
@@ -38,8 +38,7 @@ export const generatePython = ({
38
38
  ctx.matViewToClass(matview),
39
39
  );
40
40
 
41
- let output = `
42
- from __future__ import annotations
41
+ let output = `from __future__ import annotations
43
42
 
44
43
  import datetime
45
44
  import uuid
@@ -65,9 +64,9 @@ ${concatLines(py_matviews)}
65
64
 
66
65
  ${concatLines(composite_types)}
67
66
 
68
- `.trim();
67
+ `.trimEnd();
69
68
 
70
- return output;
69
+ return `${output}\n`;
71
70
  };
72
71
 
73
72
  interface Serializable {
@@ -375,7 +375,7 @@ export const generateSwift = (
375
375
  }),
376
376
  ];
377
377
 
378
- return output.join("\n");
378
+ return `${output.join("\n")}\n`;
379
379
  };
380
380
 
381
381
  // TODO: Make this more robust. Currently doesn't handle range types - returns them as string.
@@ -584,6 +584,43 @@ export const generateTypescript = async (
584
584
  return null;
585
585
  };
586
586
 
587
+ const getArgTsType = (
588
+ schema: PostgresSchema,
589
+ { name, type_id }: PostgresFunction["args"][number],
590
+ ) => {
591
+ const type = typesById.get(type_id);
592
+ if (!type) {
593
+ return "unknown";
594
+ }
595
+ const tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
596
+ // A row-typed parameter takes the relation's columns: PostgREST fills the
597
+ // keys it does not receive with NULL, and the computed fields are never
598
+ // input. A named parameter is typed without them. An unnamed one keeps the
599
+ // plain `Row` reference, which is how postgrest-js versions predating
600
+ // `ComputedFields` recognise the function as a computed field.
601
+ if (name === "") {
602
+ return tsType;
603
+ }
604
+ const relationReference = findRelationReference(
605
+ type.name,
606
+ typeContext,
607
+ type.schema,
608
+ );
609
+ if (relationReference && tsType === `${relationReference}['Row']`) {
610
+ return `Omit<${tsType}, ${relationReference}['ComputedFields']>`;
611
+ }
612
+ return tsType;
613
+ };
614
+
615
+ const getArgsTsType = (
616
+ schema: PostgresSchema,
617
+ inArgs: PostgresFunction["args"],
618
+ ) =>
619
+ `{ ${inArgs.map(
620
+ (arg) =>
621
+ `${JSON.stringify(arg.name)}${arg.has_default ? "?" : ""}: ${getArgTsType(schema, arg)}`,
622
+ )} }`;
623
+
587
624
  const getFunctionSignatures = (
588
625
  schema: PostgresSchema,
589
626
  fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction["args"] }>,
@@ -596,80 +633,21 @@ export const generateTypescript = async (
596
633
  // `never` property is uninhabited for any tool doing sound type math on the
597
634
  // generated types. `Record<PropertyKey, never>` is the accurate type for
598
635
  // "callable with no arguments".
599
- let argsType = "Record<PropertyKey, never>";
636
+ const argsType =
637
+ inArgs.length > 0
638
+ ? getArgsTsType(schema, inArgs)
639
+ : "Record<PropertyKey, never>";
600
640
  let returnType = getFunctionReturnType(schema, fn);
601
641
 
602
642
  // Check for specific error cases
603
643
  const conflictError = getConflictError(schema, fns, fn, inArgs);
604
644
  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
645
  returnType = `{ error: true } & ${JSON.stringify(conflictError)}`;
627
646
  } else if (hasTableRowError(fn, inArgs)) {
628
647
  // 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
648
  returnType = `{ error: true } & ${JSON.stringify(
651
649
  `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
650
  )}`;
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
651
  }
674
652
 
675
653
  return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }`;
@@ -753,6 +731,51 @@ export const generateTypescript = async (
753
731
  return fn.argument_types === relation.name;
754
732
  };
755
733
 
734
+ /**
735
+ * The computed fields of a relation, in `schemaFunctions` order. A function
736
+ * named like one of the relation's columns is left out: PostgREST resolves
737
+ * the name to the column, so it is neither selectable nor part of `Row`.
738
+ */
739
+ const computedFieldsOf = (
740
+ schemaFunctions: {
741
+ fn: PostgresFunction;
742
+ inArgs: PostgresFunction["args"];
743
+ }[],
744
+ relation: { id: number; name: string },
745
+ ) => {
746
+ const columnNames = new Set(
747
+ getColumns(relation.id).map((column) => column.name),
748
+ );
749
+ const seen = new Set<string>();
750
+ return schemaFunctions.filter(({ fn, inArgs }) => {
751
+ if (
752
+ !isComputedFieldOf(fn, inArgs, relation) ||
753
+ columnNames.has(fn.name) ||
754
+ seen.has(fn.name)
755
+ ) {
756
+ return false;
757
+ }
758
+ seen.add(fn.name);
759
+ return true;
760
+ });
761
+ };
762
+
763
+ const computedFieldTsDefinition = (
764
+ schema: PostgresSchema,
765
+ { fn }: { fn: PostgresFunction },
766
+ ) =>
767
+ `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
768
+ getFunctionReturnType(schema, fn),
769
+ true,
770
+ )}`;
771
+
772
+ // `ComputedFields` names the `Row` keys PostgREST leaves out of `select('*')`,
773
+ // so postgrest-js does not have to infer them from `Functions`.
774
+ const computedFieldsTsType = (computedFields: { fn: PostgresFunction }[]) =>
775
+ computedFields.length === 0
776
+ ? "never"
777
+ : computedFields.map(({ fn }) => JSON.stringify(fn.name)).join(" | ");
778
+
756
779
  let output = `
757
780
  export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
758
781
 
@@ -771,11 +794,12 @@ export type Database = {
771
794
  ${
772
795
  schemaTables.length === 0
773
796
  ? "[_ in never]: never"
774
- : schemaTables.map(
775
- ({
797
+ : schemaTables.map(({ table, relationships }) => {
798
+ const computedFields = computedFieldsOf(
799
+ schemaFunctions,
776
800
  table,
777
- relationships,
778
- }) => `${JSON.stringify(table.name)}: {
801
+ );
802
+ return `${JSON.stringify(table.name)}: {
779
803
  Row: {
780
804
  ${[
781
805
  ...getColumns(table.id).map((column) =>
@@ -791,18 +815,12 @@ export type Database = {
791
815
  typeContext,
792
816
  ),
793
817
  ),
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
- }),
818
+ ...computedFields.map((computedField) =>
819
+ computedFieldTsDefinition(schema, computedField),
820
+ ),
804
821
  ]}
805
822
  }
823
+ ComputedFields: ${computedFieldsTsType(computedFields)}
806
824
  Insert: {
807
825
  ${getColumns(table.id).map((column) => {
808
826
  if (
@@ -852,19 +870,20 @@ export type Database = {
852
870
  Relationships: [
853
871
  ${relationships.map(generateRelationshiptTsDefinition)}
854
872
  ]
855
- }`,
856
- )
873
+ }`;
874
+ })
857
875
  }
858
876
  }
859
877
  Views: {
860
878
  ${
861
879
  schemaViews.length === 0
862
880
  ? "[_ in never]: never"
863
- : schemaViews.map(
864
- ({
881
+ : schemaViews.map(({ view, relationships }) => {
882
+ const computedFields = computedFieldsOf(
883
+ schemaFunctions,
865
884
  view,
866
- relationships,
867
- }) => `${JSON.stringify(view.name)}: {
885
+ );
886
+ return `${JSON.stringify(view.name)}: {
868
887
  Row: {
869
888
  ${[
870
889
  ...getColumns(view.id).map((column) =>
@@ -880,19 +899,12 @@ export type Database = {
880
899
  typeContext,
881
900
  ),
882
901
  ),
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
- ),
902
+ ...computedFields.map((computedField) =>
903
+ computedFieldTsDefinition(schema, computedField),
904
+ ),
894
905
  ]}
895
906
  }
907
+ ComputedFields: ${computedFieldsTsType(computedFields)}
896
908
  ${
897
909
  // Metadata predating the trigger-aware flags (still valid
898
910
  // version 1 documents) falls back to the old gate.
@@ -942,8 +954,8 @@ export type Database = {
942
954
  }Relationships: [
943
955
  ${relationships.map(generateRelationshiptTsDefinition)}
944
956
  ]
945
- }`,
946
- )
957
+ }`;
958
+ })
947
959
  }
948
960
  }
949
961
  Functions: {
@@ -1157,14 +1169,7 @@ export const pgTypeToTsType = (
1157
1169
  // it whenever the owning schema is known.
1158
1170
  typeSchema?: string,
1159
1171
  ): string => {
1160
- const {
1161
- types,
1162
- schemas,
1163
- tables,
1164
- views,
1165
- foreignTables = [],
1166
- materializedViews = [],
1167
- } = context;
1172
+ const { types, schemas } = context;
1168
1173
  if (pgType === "bool") {
1169
1174
  return "boolean";
1170
1175
  } else if (
@@ -1228,42 +1233,63 @@ export const pgTypeToTsType = (
1228
1233
  return "unknown";
1229
1234
  }
1230
1235
 
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
- }
1236
+ const relationReference = findRelationReference(
1237
+ pgType,
1238
+ context,
1239
+ preferredSchema,
1240
+ );
1241
+ return relationReference ? `${relationReference}['Row']` : "unknown";
1242
+ }
1243
+ };
1266
1244
 
1267
- return "unknown";
1245
+ /**
1246
+ * The `Database[schema][kind][name]` reference of the relation whose row type
1247
+ * is `pgType`, or `undefined` when no generated relation has that type.
1248
+ *
1249
+ * A relation-typed value carries only a bare type name, so several relations
1250
+ * can share it and `preferredSchema` is what disambiguates them. Match on the
1251
+ * schema across every relation kind before falling back to kind order,
1252
+ * otherwise a relation in an unrelated schema outranks the one the caller
1253
+ * meant. Foreign tables are generated under `Tables` and materialized views
1254
+ * under `Views`, alongside their plain counterparts.
1255
+ */
1256
+ const findRelationReference = (
1257
+ pgType: string,
1258
+ context: TypeResolutionContext,
1259
+ preferredSchema: string,
1260
+ ): string | undefined => {
1261
+ const {
1262
+ schemas,
1263
+ tables,
1264
+ views,
1265
+ foreignTables = [],
1266
+ materializedViews = [],
1267
+ } = context;
1268
+ const relationKinds = [
1269
+ ["Tables", tables],
1270
+ ["Tables", foreignTables],
1271
+ ["Views", views],
1272
+ ["Views", materializedViews],
1273
+ ] as const;
1274
+ const findRelation = (
1275
+ matches: (relation: { name: string; schema: string }) => boolean,
1276
+ ) => {
1277
+ for (const [kind, relations] of relationKinds) {
1278
+ const relation = relations.find(matches);
1279
+ if (relation) return { kind, relation };
1280
+ }
1281
+ return undefined;
1282
+ };
1283
+ const match =
1284
+ findRelation(
1285
+ ({ name, schema: relationSchema }) =>
1286
+ name === pgType && relationSchema === preferredSchema,
1287
+ ) ?? findRelation(({ name }) => name === pgType);
1288
+ if (!match || !schemas.some(({ name }) => name === match.relation.schema)) {
1289
+ return undefined;
1268
1290
  }
1291
+ const { kind, relation } = match;
1292
+ return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(
1293
+ relation.name,
1294
+ )}]`;
1269
1295
  };