Skip to content

Commit d8b677f

Browse files
committed
feat(types): add embeded functions type inference
1 parent 823f28f commit d8b677f

18 files changed

+3848
-5464
lines changed

package-lock.json

Lines changed: 1 addition & 4831 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/PostgrestClient.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import PostgrestQueryBuilder from './PostgrestQueryBuilder'
22
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
3-
import { Fetch, GenericSchema, ClientServerOptions, GetGenericDatabaseWithOptions } from './types'
3+
import {
4+
Fetch,
5+
GenericSchema,
6+
ClientServerOptions,
7+
GetGenericDatabaseWithOptions,
8+
GetRpcFunctionFilterBuilderByArgs,
9+
} from './types'
410

511
/**
612
* PostgREST client.
@@ -124,9 +130,17 @@ export default class PostgrestClient<
124130
* `"estimated"`: Uses exact count for low numbers and planned count for high
125131
* numbers.
126132
*/
127-
rpc<FnName extends string & keyof Schema['Functions'], Fn extends Schema['Functions'][FnName]>(
133+
rpc<
134+
FnName extends string & keyof Schema['Functions'],
135+
Args extends Schema['Functions'][FnName]['Args'] = never,
136+
FilterBuilder extends GetRpcFunctionFilterBuilderByArgs<
137+
Schema,
138+
FnName,
139+
Args
140+
> = GetRpcFunctionFilterBuilderByArgs<Schema, FnName, Args>
141+
>(
128142
fn: FnName,
129-
args: Fn['Args'] = {},
143+
args: Args = {} as Args,
130144
{
131145
head = false,
132146
get = false,
@@ -139,14 +153,10 @@ export default class PostgrestClient<
139153
): PostgrestFilterBuilder<
140154
ClientOptions,
141155
Schema,
142-
Fn['Returns'] extends any[]
143-
? Fn['Returns'][number] extends Record<string, unknown>
144-
? Fn['Returns'][number]
145-
: never
146-
: never,
147-
Fn['Returns'],
148-
FnName,
149-
null,
156+
FilterBuilder['Row'],
157+
FilterBuilder['Result'],
158+
FilterBuilder['RelationName'],
159+
FilterBuilder['Relationships'],
150160
'RPC'
151161
> {
152162
let method: 'HEAD' | 'GET' | 'POST'

src/PostgrestTransformBuilder.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export default class PostgrestTransformBuilder<
3535
ClientOptions,
3636
Schema,
3737
Row,
38-
NewResultOne[],
38+
Method extends 'RPC'
39+
? Result extends unknown[]
40+
? NewResultOne[]
41+
: NewResultOne
42+
: NewResultOne[],
3943
RelationName,
4044
Relationships,
4145
Method
@@ -60,7 +64,11 @@ export default class PostgrestTransformBuilder<
6064
ClientOptions,
6165
Schema,
6266
Row,
63-
NewResultOne[],
67+
Method extends 'RPC'
68+
? Result extends unknown[]
69+
? NewResultOne[]
70+
: NewResultOne
71+
: NewResultOne[],
6472
RelationName,
6573
Relationships,
6674
Method

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type {
3030
PostgrestMaybeSingleResponse,
3131
ClientServerOptions,
3232
GetGenericDatabaseWithOptions,
33+
GetRpcFunctionFilterBuilderByArgs,
3334
} from './types'
3435
// https://github.com/supabase/postgrest-js/issues/551
3536
// To be replaced with a helper type that only uses public types

src/select-query-parser/result.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ export type ProcessEmbeddedResource<
364364
> = ResolveRelationship<Schema, Relationships, Field, CurrentTableOrView> extends infer Resolved
365365
? Resolved extends {
366366
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
367-
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' }
367+
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
368368
direction: string
369369
}
370370
? ProcessEmbeddedResourceResult<ClientOptions, Schema, Resolved, Field, CurrentTableOrView>
@@ -383,7 +383,12 @@ type ProcessEmbeddedResourceResult<
383383
Schema extends GenericSchema,
384384
Resolved extends {
385385
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
386-
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' }
386+
relation: GenericRelationship & {
387+
match: 'refrel' | 'col' | 'fkname' | 'func'
388+
isNotNullable?: boolean
389+
referencedRelation: string
390+
isSetofReturn?: boolean
391+
}
387392
direction: string
388393
},
389394
Field extends Ast.FieldNode,
@@ -392,7 +397,11 @@ type ProcessEmbeddedResourceResult<
392397
ClientOptions,
393398
Schema,
394399
Resolved['referencedTable']['Row'],
395-
Field['name'],
400+
// For embeded function selection, the source of truth is the 'referencedRelation'
401+
// coming from the SetofOptions.to parameter
402+
Resolved['relation']['match'] extends 'func'
403+
? Resolved['relation']['referencedRelation']
404+
: Field['name'],
396405
Resolved['referencedTable']['Relationships'],
397406
Field['children'] extends undefined
398407
? []
@@ -407,7 +416,18 @@ type ProcessEmbeddedResourceResult<
407416
? ProcessedChildren
408417
: ProcessedChildren[]
409418
: Resolved['relation']['isOneToOne'] extends true
410-
? ProcessedChildren | null
419+
? Resolved['relation']['match'] extends 'func'
420+
? Resolved['relation']['isNotNullable'] extends true
421+
? Resolved['relation']['isSetofReturn'] extends true
422+
? ProcessedChildren
423+
: // TODO: This shouldn't be necessary but is due in an inconsitency in PostgREST v12/13 where if a function
424+
// is declared with RETURNS <table-name> instead of RETURNS SETOF <table-name> ROWS 1
425+
// In case where there is no object matching the relations, the object will be returned with all the properties within it
426+
// set to null, we mimic this buggy behavior for type safety an issue is opened on postgREST here:
427+
// https://github.com/PostgREST/postgrest/issues/4234
428+
{ [P in keyof ProcessedChildren]: ProcessedChildren[P] | null }
429+
: ProcessedChildren | null
430+
: ProcessedChildren | null
411431
: ProcessedChildren[]
412432
: // If the relation is a self-reference it'll always be considered as reverse relationship
413433
Resolved['relation']['referencedRelation'] extends CurrentTableOrView

src/select-query-parser/utils.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GenericFunction, GenericSetofOption } from '../types'
12
import { Ast } from './parser'
23
import {
34
AggregateFunctions,
@@ -452,6 +453,36 @@ export type ResolveForwardRelationship<
452453
from: CurrentTableOrView
453454
type: 'found-by-join-table'
454455
}
456+
: ResolveEmbededFunctionJoinTableRelationship<
457+
Schema,
458+
CurrentTableOrView,
459+
Field['name']
460+
> extends infer FoundEmbededFunctionJoinTableRelation
461+
? FoundEmbededFunctionJoinTableRelation extends GenericSetofOption
462+
? {
463+
referencedTable: TablesAndViews<Schema>[FoundEmbededFunctionJoinTableRelation['to']]
464+
relation: {
465+
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_${FoundEmbededFunctionJoinTableRelation['to']}_forward`
466+
columns: []
467+
isOneToOne: FoundEmbededFunctionJoinTableRelation['isOneToOne'] extends true
468+
? true
469+
: false
470+
referencedColumns: []
471+
referencedRelation: FoundEmbededFunctionJoinTableRelation['to']
472+
} & {
473+
match: 'func'
474+
isNotNullable: FoundEmbededFunctionJoinTableRelation['isNotNullable'] extends true
475+
? true
476+
: FoundEmbededFunctionJoinTableRelation['isSetofReturn'] extends true
477+
? false
478+
: true
479+
isSetofReturn: FoundEmbededFunctionJoinTableRelation['isSetofReturn']
480+
}
481+
direction: 'forward'
482+
from: CurrentTableOrView
483+
type: 'found-by-embeded-function'
484+
}
485+
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
455486
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
456487
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
457488
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
@@ -495,6 +526,19 @@ type ResolveJoinTableRelationship<
495526
: never
496527
}[keyof TablesAndViews<Schema>]
497528

529+
type ResolveEmbededFunctionJoinTableRelationship<
530+
Schema extends GenericSchema,
531+
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
532+
FieldName extends string
533+
> = FindMatchingFunctionBySetofFrom<
534+
Schema['Functions'][FieldName],
535+
CurrentTableOrView
536+
> extends infer Fn
537+
? Fn extends GenericFunction
538+
? Fn['SetofOptions']
539+
: false
540+
: false
541+
498542
export type FindJoinTableRelationship<
499543
Schema extends GenericSchema,
500544
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
@@ -579,6 +623,50 @@ export type IsStringUnion<T> = string extends T
579623
: true
580624
: false
581625

626+
// Functions matching utils
627+
export type IsMatchingArgs<
628+
FnArgs extends GenericFunction['Args'],
629+
PassedArgs extends GenericFunction['Args']
630+
> = [FnArgs] extends [Record<PropertyKey, never>]
631+
? PassedArgs extends Record<PropertyKey, never>
632+
? true
633+
: false
634+
: keyof PassedArgs extends keyof FnArgs
635+
? PassedArgs extends FnArgs
636+
? true
637+
: false
638+
: false
639+
640+
export type MatchingFunctionArgs<
641+
Fn extends GenericFunction,
642+
Args extends GenericFunction['Args']
643+
> = Fn extends { Args: infer A extends GenericFunction['Args'] }
644+
? IsMatchingArgs<A, Args> extends true
645+
? Fn
646+
: never
647+
: never
648+
649+
export type FindMatchingFunctionByArgs<
650+
FnUnion,
651+
Args extends GenericFunction['Args']
652+
> = FnUnion extends infer Fn extends GenericFunction ? MatchingFunctionArgs<Fn, Args> : never
653+
654+
type MatchingFunctionBySetofFrom<
655+
Fn extends GenericFunction,
656+
TableName extends string
657+
> = Fn['SetofOptions'] extends GenericSetofOption
658+
? TableName extends Fn['SetofOptions']['from']
659+
? Fn
660+
: never
661+
: never
662+
663+
type FindMatchingFunctionBySetofFrom<
664+
FnUnion,
665+
TableName extends string
666+
> = FnUnion extends infer Fn extends GenericFunction
667+
? MatchingFunctionBySetofFrom<Fn, TableName>
668+
: false
669+
582670
type ComputedField<
583671
Schema extends GenericSchema,
584672
RelationName extends keyof TablesAndViews<Schema>,

src/types.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,82 @@
11
import PostgrestError from './PostgrestError'
22
import { ContainsNull } from './select-query-parser/types'
3-
import { IsAny, SelectQueryError } from './select-query-parser/utils'
3+
import { FindMatchingFunctionByArgs, IsAny, SelectQueryError } from './select-query-parser/utils'
4+
import { LastOf } from './select-query-parser/types'
45

56
export type Fetch = typeof fetch
67

8+
type ExactMatch<T, S> = [T] extends [S] ? ([S] extends [T] ? true : false) : false
9+
10+
type ExtractExactFunction<Fns, Args> = Fns extends infer F
11+
? F extends GenericFunction
12+
? ExactMatch<F['Args'], Args> extends true
13+
? F
14+
: never
15+
: never
16+
: never
17+
18+
type IsNever<T> = [T] extends [never] ? true : false
19+
20+
export type GetRpcFunctionFilterBuilderByArgs<
21+
Schema extends GenericSchema,
22+
FnName extends string & keyof Schema['Functions'],
23+
Args
24+
> = {
25+
0: Schema['Functions'][FnName]
26+
// If the Args is exactly never (function call without any params)
27+
1: IsAny<Schema> extends true
28+
? any
29+
: IsNever<Args> extends true
30+
? ExtractExactFunction<Schema['Functions'][FnName], Args>
31+
: // Otherwise, we attempt to match with one of the function definition in the union based
32+
// on the function arguments provided
33+
Args extends GenericFunction['Args']
34+
? LastOf<FindMatchingFunctionByArgs<Schema['Functions'][FnName], Args>>
35+
: // If we can't find a matching function by args, we try to find one by function name
36+
ExtractExactFunction<Schema['Functions'][FnName], Args> extends GenericFunction
37+
? ExtractExactFunction<Schema['Functions'][FnName], Args>
38+
: any
39+
}[1] extends infer Fn
40+
? // If we are dealing with an non-typed client everything is any
41+
IsAny<Fn> extends true
42+
? { Row: any; Result: any; RelationName: FnName; Relationships: null }
43+
: // Otherwise, we use the arguments based function definition narrowing to get the rigt value
44+
Fn extends GenericFunction
45+
? {
46+
Row: Fn['Returns'] extends any[]
47+
? Fn['Returns'][number] extends Record<string, unknown>
48+
? Fn['Returns'][number]
49+
: never
50+
: Fn['Returns'] extends Record<string, unknown>
51+
? Fn['Returns']
52+
: never
53+
Result: Fn['SetofOptions'] extends GenericSetofOption
54+
? Fn['SetofOptions']['isSetofReturn'] extends true
55+
? Fn['SetofOptions']['isOneToOne'] extends true
56+
? Fn['Returns'][]
57+
: Fn['Returns']
58+
: Fn['Returns']
59+
: Fn['Returns']
60+
RelationName: Fn['SetofOptions'] extends GenericSetofOption
61+
? Fn['SetofOptions']['to']
62+
: FnName
63+
Relationships: Fn['SetofOptions'] extends GenericSetofOption
64+
? Fn['SetofOptions']['to'] extends keyof Schema['Tables']
65+
? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships']
66+
: Schema['Views'][Fn['SetofOptions']['to']]['Relationships']
67+
: null
68+
}
69+
: // If we failed to find the function by argument, we still pass with any but also add an overridable
70+
Fn extends false
71+
? {
72+
Row: any
73+
Result: { error: true } & "Couldn't infer function definition matching provided arguments"
74+
RelationName: FnName
75+
Relationships: null
76+
}
77+
: never
78+
: never
79+
780
/**
881
* Response format
982
*
@@ -60,9 +133,18 @@ export type GenericNonUpdatableView = {
60133

61134
export type GenericView = GenericUpdatableView | GenericNonUpdatableView
62135

136+
export type GenericSetofOption = {
137+
isSetofReturn?: boolean | undefined
138+
isOneToOne?: boolean | undefined
139+
isNotNullable?: boolean | undefined
140+
to: string
141+
from: string
142+
}
143+
63144
export type GenericFunction = {
64-
Args: Record<string, unknown>
145+
Args: Record<string, unknown> | never
65146
Returns: unknown
147+
SetofOptions?: GenericSetofOption
66148
}
67149

68150
export type GenericSchema = {

0 commit comments

Comments
 (0)