Skip to content

Commit b9753a1

Browse files
authored
feat(postgrest): leave computed fields out of select('*') and resolve no-argument rpc overloads for postgrest-typegen output (#2730)
1 parent 88faa11 commit b9753a1

8 files changed

Lines changed: 586 additions & 22 deletions

File tree

‎packages/core/postgrest-js/src/select-query-parser/result.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ export type ProcessEmbeddedResource<
383383
}
384384
: Resolved extends {
385385
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
386+
relationName: string
386387
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
387388
direction: string
388389
}
@@ -412,6 +413,7 @@ type ProcessEmbeddedResourceResult<
412413
Schema extends GenericSchema,
413414
Resolved extends {
414415
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
416+
relationName: string
415417
relation: GenericRelationship & {
416418
match: 'refrel' | 'col' | 'fkname' | 'func'
417419
isNotNullable?: boolean
@@ -427,11 +429,9 @@ type ProcessEmbeddedResourceResult<
427429
ClientOptions,
428430
Schema,
429431
Resolved['referencedTable']['Row'],
430-
// For embeded function selection, the source of truth is the 'referencedRelation'
431-
// coming from the SetofOptions.to parameter
432-
Resolved['relation']['match'] extends 'func'
433-
? Resolved['relation']['referencedRelation']
434-
: Field['name'],
432+
// The embedded relation's own name, which differs from the field name when the embed
433+
// goes through a column or foreign key name (`username(*)`) or an embedded function
434+
Resolved['relationName'],
435435
Resolved['referencedTable']['Relationships'],
436436
Field['children'] extends undefined
437437
? []

‎packages/core/postgrest-js/src/select-query-parser/utils.ts‎

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ type ResolveReverseRelationship<
266266
FoundRelation extends { hint: string } | { match: 'col' }
267267
? {
268268
referencedTable: TablesAndViews<Schema>[RelatedRelationName]
269+
relationName: RelatedRelationName
269270
relation: FoundRelation
270271
direction: 'reverse'
271272
from: CurrentTableOrView
@@ -275,6 +276,7 @@ type ResolveReverseRelationship<
275276
? SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}!<columnName> ?`>
276277
: {
277278
referencedTable: TablesAndViews<Schema>[RelatedRelationName]
279+
relationName: RelatedRelationName
278280
relation: FoundRelation
279281
direction: 'reverse'
280282
from: CurrentTableOrView
@@ -428,6 +430,7 @@ export type ResolveForwardRelationship<
428430
? FoundByName extends GenericRelationship
429431
? {
430432
referencedTable: TablesAndViews<Schema>[Field['name']]
433+
relationName: Field['name']
431434
relation: FoundByName
432435
direction: 'forward'
433436
from: Field['name']
@@ -443,6 +446,7 @@ export type ResolveForwardRelationship<
443446
}
444447
? {
445448
referencedTable: TablesAndViews<Schema>[FoundByMatch['from']]
449+
relationName: FoundByMatch['from'] & string
446450
relation: FoundByMatch
447451
direction: 'forward'
448452
from: CurrentTableOrView
@@ -456,6 +460,7 @@ export type ResolveForwardRelationship<
456460
? FoundByJoinTable extends GenericRelationship
457461
? {
458462
referencedTable: TablesAndViews<Schema>[FoundByJoinTable['referencedRelation']]
463+
relationName: FoundByJoinTable['referencedRelation']
459464
relation: FoundByJoinTable & { match: 'refrel' }
460465
direction: 'forward'
461466
from: CurrentTableOrView
@@ -491,6 +496,7 @@ export type ResolveForwardRelationship<
491496
scalarType: FoundEmbededFunctionJoinTableRelation['Returns']
492497
direction: 'forward'
493498
from: CurrentTableOrView
499+
relationName: ''
494500
type: 'found-by-embeded-scalar-function'
495501
}
496502
: // Table-valued function: `to` names the target table/view.
@@ -515,6 +521,7 @@ export type ResolveForwardRelationship<
515521
}
516522
direction: 'forward'
517523
from: CurrentTableOrView
524+
relationName: FoundEmbededFunctionJoinTableRelation['SetofOptions']['to']
518525
type: 'found-by-embeded-function'
519526
}
520527
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
@@ -681,26 +688,73 @@ type FindMatchingFunctionBySetofFrom<
681688
? MatchingFunctionBySetofFrom<Fn, TableName>
682689
: false
683690

691+
type IsUnion<T, U = T> = [T] extends [never]
692+
? false
693+
: T extends U
694+
? [U] extends [T]
695+
? false
696+
: true
697+
: never
698+
699+
type SameKeys<A, B> = [keyof A] extends [keyof B]
700+
? [keyof B] extends [keyof A]
701+
? true
702+
: false
703+
: false
704+
705+
// The type of a function's only argument when it has exactly one and it is required,
706+
// whatever the argument is called. `Args: never` (a zero-argument function in older
707+
// generated types), `Args: Record<PropertyKey, never>`, an optional argument and several
708+
// arguments all resolve to `never`.
709+
type SoleRequiredArgument<Args> = [Args] extends [never]
710+
? never
711+
: IsUnion<keyof Args> extends true
712+
? never
713+
: {} extends Args
714+
? never
715+
: Args[keyof Args]
716+
717+
// Whether one signature of a function (one member of an overload union, as `Fn` is a
718+
// naked type parameter and distributes) is a computed field of a relation: PostgREST
719+
// treats a function whose only argument is the relation's row type as one. The argument
720+
// is matched on its keys rather than its exact type, so that a `Row` whose column types
721+
// were overridden (`MergeDeep`) still matches the generated `Row` the argument references.
722+
type IsComputedFieldFunction<Fn, Row> = Fn extends { Args: infer Args }
723+
? [SoleRequiredArgument<Args>] extends [never]
724+
? false
725+
: SameKeys<SoleRequiredArgument<Args>, Row>
726+
: false
727+
684728
type ComputedField<
685729
Schema extends GenericSchema,
686730
RelationName extends keyof TablesAndViews<Schema>,
687731
FieldName extends keyof TablesAndViews<Schema>[RelationName]['Row'],
688732
> = FieldName extends keyof Schema['Functions']
689-
? [Schema['Functions'][FieldName]['Args']] extends [never]
690-
? never
691-
: Schema['Functions'][FieldName] extends {
692-
Args: { '': TablesAndViews<Schema>[RelationName]['Row'] }
693-
Returns: any
694-
}
695-
? FieldName
696-
: never
733+
? true extends IsComputedFieldFunction<
734+
Schema['Functions'][FieldName],
735+
TablesAndViews<Schema>[RelationName]['Row']
736+
>
737+
? FieldName
738+
: never
697739
: never
698740

699-
// Given a relation name (Table or View) extract all the "computed fields" based on the Row
700-
// object, and the schema functions definitions
701-
export type GetComputedFields<
741+
// Computed fields of a relation inferred from its `Row` keys and the schema functions, for
742+
// types generated before the relation declared them in `ComputedFields`.
743+
type InferredComputedFields<
702744
Schema extends GenericSchema,
703745
RelationName extends keyof TablesAndViews<Schema>,
704746
> = {
705747
[K in keyof TablesAndViews<Schema>[RelationName]['Row']]: ComputedField<Schema, RelationName, K>
706748
}[keyof TablesAndViews<Schema>[RelationName]['Row']]
749+
750+
// Given a relation name (Table or View) extract all the "computed fields", which PostgREST
751+
// leaves out of a `*` selection: the names the relation declares in `ComputedFields`, or
752+
// otherwise the ones inferred from the schema functions definitions.
753+
export type GetComputedFields<
754+
Schema extends GenericSchema,
755+
RelationName extends keyof TablesAndViews<Schema>,
756+
> = 'ComputedFields' extends keyof TablesAndViews<Schema>[RelationName]
757+
? TablesAndViews<Schema>[RelationName] extends { ComputedFields?: infer Fields extends string }
758+
? Fields
759+
: never
760+
: InferredComputedFields<Schema, RelationName>

‎packages/core/postgrest-js/src/types/common/common.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,26 @@ export type GenericTable = {
4242
Insert: Record<string, unknown>
4343
Update: Record<string, unknown>
4444
Relationships: GenericRelationship[]
45+
/**
46+
* Names of the `Row` keys that are computed fields (functions taking the row), which
47+
* PostgREST leaves out of `select('*')`. Generated types declare it; when absent, the
48+
* computed fields are inferred from `Functions`.
49+
*/
50+
ComputedFields?: string
4551
}
4652

4753
export type GenericUpdatableView = {
4854
Row: Record<string, unknown>
4955
Insert: Record<string, unknown>
5056
Update: Record<string, unknown>
5157
Relationships: GenericRelationship[]
58+
ComputedFields?: string
5259
}
5360

5461
export type GenericNonUpdatableView = {
5562
Row: Record<string, unknown>
5663
Relationships: GenericRelationship[]
64+
ComputedFields?: string
5765
}
5866

5967
export type GenericView = GenericUpdatableView | GenericNonUpdatableView

‎packages/core/postgrest-js/src/types/common/rpc.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,16 @@ export type GetRpcFunctionFilterBuilderByArgs<
7676
1: IsAny<Schema> extends true
7777
? any
7878
: IsNever<Args> extends true
79-
? // This is for retro compatibility, if the funcition is defined with an single return and an union of Args
80-
// we fallback to the last function definition matched by name
79+
? // Generated types spell a zero-argument definition either `Args: never` or
80+
// `Args: Record<PropertyKey, never>`, so both are matched before falling back to the last
81+
// function definition matched by name, which is for retro compatibility with a single
82+
// return and an union of Args
8183
IsNever<ExtractExactFunction<Schema['Functions'][FnName], Args>> extends true
82-
? LastOf<Schema['Functions'][FnName]>
84+
? IsNever<
85+
ExtractExactFunction<Schema['Functions'][FnName], Record<PropertyKey, never>>
86+
> extends true
87+
? LastOf<Schema['Functions'][FnName]>
88+
: ExtractExactFunction<Schema['Functions'][FnName], Record<PropertyKey, never>>
8389
: ExtractExactFunction<Schema['Functions'][FnName], Args>
8490
: Args extends Record<PropertyKey, never>
8591
? LastOf<Schema['Functions'][FnName]>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expectType, TypeEqual } from './types'
2+
import { PostgrestClient } from '../src/index'
3+
4+
type Database = {
5+
public: {
6+
Tables: {
7+
[_ in never]: never
8+
}
9+
Views: {
10+
[_ in never]: never
11+
}
12+
Functions: {
13+
overloaded_never: { Args: never; Returns: number } | { Args: { a: string }; Returns: string }
14+
overloaded_record:
15+
| { Args: Record<PropertyKey, never>; Returns: number }
16+
| { Args: { a: string }; Returns: string }
17+
single_record: { Args: Record<PropertyKey, never>; Returns: boolean }
18+
}
19+
Enums: {
20+
[_ in never]: never
21+
}
22+
CompositeTypes: {
23+
[_ in never]: never
24+
}
25+
}
26+
}
27+
28+
const postgrest = new PostgrestClient<Database>('http://localhost:54321/rest/v1')
29+
30+
// A zero-argument overload typed `Args: never` resolves for a call without arguments
31+
{
32+
const { data } = await postgrest.rpc('overloaded_never')
33+
expectType<TypeEqual<typeof data, number | null>>(true)
34+
}
35+
36+
// A zero-argument overload typed `Args: Record<PropertyKey, never>` resolves the same way
37+
{
38+
const { data } = await postgrest.rpc('overloaded_record')
39+
expectType<TypeEqual<typeof data, number | null>>(true)
40+
}
41+
42+
// Passing arguments still selects the matching overload
43+
{
44+
const { data } = await postgrest.rpc('overloaded_record', { a: 'value' })
45+
expectType<TypeEqual<typeof data, string | null>>(true)
46+
}
47+
48+
// A function that is not overloaded is unaffected
49+
{
50+
const { data } = await postgrest.rpc('single_record')
51+
expectType<TypeEqual<typeof data, boolean | null>>(true)
52+
}

0 commit comments

Comments
 (0)