Skip to content

feat(types): add embeded functions type inference #632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,832 changes: 1 addition & 4,831 deletions package-lock.json

Large diffs are not rendered by default.

32 changes: 21 additions & 11 deletions src/PostgrestClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import PostgrestQueryBuilder from './PostgrestQueryBuilder'
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
import { Fetch, GenericSchema, ClientServerOptions, GetGenericDatabaseWithOptions } from './types'
import {
Fetch,
GenericSchema,
ClientServerOptions,
GetGenericDatabaseWithOptions,
GetRpcFunctionFilterBuilderByArgs,
} from './types'

/**
* PostgREST client.
Expand Down Expand Up @@ -124,9 +130,17 @@ export default class PostgrestClient<
* `"estimated"`: Uses exact count for low numbers and planned count for high
* numbers.
*/
rpc<FnName extends string & keyof Schema['Functions'], Fn extends Schema['Functions'][FnName]>(
rpc<
FnName extends string & keyof Schema['Functions'],
Args extends Schema['Functions'][FnName]['Args'] = never,
FilterBuilder extends GetRpcFunctionFilterBuilderByArgs<
Schema,
FnName,
Args
> = GetRpcFunctionFilterBuilderByArgs<Schema, FnName, Args>
>(
fn: FnName,
args: Fn['Args'] = {},
args: Args = {} as Args,
{
head = false,
get = false,
Expand All @@ -139,14 +153,10 @@ export default class PostgrestClient<
): PostgrestFilterBuilder<
ClientOptions,
Schema,
Fn['Returns'] extends any[]
? Fn['Returns'][number] extends Record<string, unknown>
? Fn['Returns'][number]
: never
: never,
Fn['Returns'],
FnName,
null,
FilterBuilder['Row'],
FilterBuilder['Result'],
FilterBuilder['RelationName'],
FilterBuilder['Relationships'],
'RPC'
> {
let method: 'HEAD' | 'GET' | 'POST'
Expand Down
12 changes: 10 additions & 2 deletions src/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export default class PostgrestTransformBuilder<
ClientOptions,
Schema,
Row,
NewResultOne[],
Method extends 'RPC'
? Result extends unknown[]
? NewResultOne[]
: NewResultOne
: NewResultOne[],
RelationName,
Relationships,
Method
Expand All @@ -60,7 +64,11 @@ export default class PostgrestTransformBuilder<
ClientOptions,
Schema,
Row,
NewResultOne[],
Method extends 'RPC'
? Result extends unknown[]
? NewResultOne[]
: NewResultOne
: NewResultOne[],
RelationName,
Relationships,
Method
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type {
PostgrestMaybeSingleResponse,
ClientServerOptions,
GetGenericDatabaseWithOptions,
GetRpcFunctionFilterBuilderByArgs,
} from './types'
// https://github.com/supabase/postgrest-js/issues/551
// To be replaced with a helper type that only uses public types
Expand Down
28 changes: 24 additions & 4 deletions src/select-query-parser/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ export type ProcessEmbeddedResource<
> = ResolveRelationship<Schema, Relationships, Field, CurrentTableOrView> extends infer Resolved
? Resolved extends {
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' }
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
direction: string
}
? ProcessEmbeddedResourceResult<ClientOptions, Schema, Resolved, Field, CurrentTableOrView>
Expand All @@ -383,7 +383,12 @@ type ProcessEmbeddedResourceResult<
Schema extends GenericSchema,
Resolved extends {
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' }
relation: GenericRelationship & {
match: 'refrel' | 'col' | 'fkname' | 'func'
isNotNullable?: boolean
referencedRelation: string
isSetofReturn?: boolean
}
direction: string
},
Field extends Ast.FieldNode,
Expand All @@ -392,7 +397,11 @@ type ProcessEmbeddedResourceResult<
ClientOptions,
Schema,
Resolved['referencedTable']['Row'],
Field['name'],
// For embeded function selection, the source of truth is the 'referencedRelation'
// coming from the SetofOptions.to parameter
Resolved['relation']['match'] extends 'func'
? Resolved['relation']['referencedRelation']
: Field['name'],
Resolved['referencedTable']['Relationships'],
Field['children'] extends undefined
? []
Expand All @@ -407,7 +416,18 @@ type ProcessEmbeddedResourceResult<
? ProcessedChildren
: ProcessedChildren[]
: Resolved['relation']['isOneToOne'] extends true
? ProcessedChildren | null
? Resolved['relation']['match'] extends 'func'
? Resolved['relation']['isNotNullable'] extends true
? Resolved['relation']['isSetofReturn'] extends true
? ProcessedChildren
: // TODO: This shouldn't be necessary but is due in an inconsitency in PostgREST v12/13 where if a function
// is declared with RETURNS <table-name> instead of RETURNS SETOF <table-name> ROWS 1
// In case where there is no object matching the relations, the object will be returned with all the properties within it
// set to null, we mimic this buggy behavior for type safety an issue is opened on postgREST here:
// https://github.com/PostgREST/postgrest/issues/4234
{ [P in keyof ProcessedChildren]: ProcessedChildren[P] | null }
: ProcessedChildren | null
: ProcessedChildren | null
: ProcessedChildren[]
: // If the relation is a self-reference it'll always be considered as reverse relationship
Resolved['relation']['referencedRelation'] extends CurrentTableOrView
Expand Down
88 changes: 88 additions & 0 deletions src/select-query-parser/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GenericFunction, GenericSetofOption } from '../types'
import { Ast } from './parser'
import {
AggregateFunctions,
Expand Down Expand Up @@ -452,6 +453,36 @@ export type ResolveForwardRelationship<
from: CurrentTableOrView
type: 'found-by-join-table'
}
: ResolveEmbededFunctionJoinTableRelationship<
Schema,
CurrentTableOrView,
Field['name']
> extends infer FoundEmbededFunctionJoinTableRelation
? FoundEmbededFunctionJoinTableRelation extends GenericSetofOption
? {
referencedTable: TablesAndViews<Schema>[FoundEmbededFunctionJoinTableRelation['to']]
relation: {
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_${FoundEmbededFunctionJoinTableRelation['to']}_forward`
columns: []
isOneToOne: FoundEmbededFunctionJoinTableRelation['isOneToOne'] extends true
? true
: false
referencedColumns: []
referencedRelation: FoundEmbededFunctionJoinTableRelation['to']
} & {
match: 'func'
isNotNullable: FoundEmbededFunctionJoinTableRelation['isNotNullable'] extends true
? true
: FoundEmbededFunctionJoinTableRelation['isSetofReturn'] extends true
? false
: true
isSetofReturn: FoundEmbededFunctionJoinTableRelation['isSetofReturn']
}
direction: 'forward'
from: CurrentTableOrView
type: 'found-by-embeded-function'
}
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
Expand Down Expand Up @@ -495,6 +526,19 @@ type ResolveJoinTableRelationship<
: never
}[keyof TablesAndViews<Schema>]

type ResolveEmbededFunctionJoinTableRelationship<
Schema extends GenericSchema,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
FieldName extends string
> = FindMatchingFunctionBySetofFrom<
Schema['Functions'][FieldName],
CurrentTableOrView
> extends infer Fn
? Fn extends GenericFunction
? Fn['SetofOptions']
: false
: false

export type FindJoinTableRelationship<
Schema extends GenericSchema,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
Expand Down Expand Up @@ -579,6 +623,50 @@ export type IsStringUnion<T> = string extends T
: true
: false

// Functions matching utils
export type IsMatchingArgs<
FnArgs extends GenericFunction['Args'],
PassedArgs extends GenericFunction['Args']
> = [FnArgs] extends [Record<PropertyKey, never>]
? PassedArgs extends Record<PropertyKey, never>
? true
: false
: keyof PassedArgs extends keyof FnArgs
? PassedArgs extends FnArgs
? true
: false
: false

export type MatchingFunctionArgs<
Fn extends GenericFunction,
Args extends GenericFunction['Args']
> = Fn extends { Args: infer A extends GenericFunction['Args'] }
? IsMatchingArgs<A, Args> extends true
? Fn
: never
: never

export type FindMatchingFunctionByArgs<
FnUnion,
Args extends GenericFunction['Args']
> = FnUnion extends infer Fn extends GenericFunction ? MatchingFunctionArgs<Fn, Args> : never

type MatchingFunctionBySetofFrom<
Fn extends GenericFunction,
TableName extends string
> = Fn['SetofOptions'] extends GenericSetofOption
? TableName extends Fn['SetofOptions']['from']
? Fn
: never
: never

type FindMatchingFunctionBySetofFrom<
FnUnion,
TableName extends string
> = FnUnion extends infer Fn extends GenericFunction
? MatchingFunctionBySetofFrom<Fn, TableName>
: false

type ComputedField<
Schema extends GenericSchema,
RelationName extends keyof TablesAndViews<Schema>,
Expand Down
86 changes: 84 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,82 @@
import PostgrestError from './PostgrestError'
import { ContainsNull } from './select-query-parser/types'
import { IsAny, SelectQueryError } from './select-query-parser/utils'
import { FindMatchingFunctionByArgs, IsAny, SelectQueryError } from './select-query-parser/utils'
import { LastOf } from './select-query-parser/types'

export type Fetch = typeof fetch

type ExactMatch<T, S> = [T] extends [S] ? ([S] extends [T] ? true : false) : false

type ExtractExactFunction<Fns, Args> = Fns extends infer F
? F extends GenericFunction
? ExactMatch<F['Args'], Args> extends true
? F
: never
: never
: never

type IsNever<T> = [T] extends [never] ? true : false

export type GetRpcFunctionFilterBuilderByArgs<
Schema extends GenericSchema,
FnName extends string & keyof Schema['Functions'],
Args
> = {
0: Schema['Functions'][FnName]
// If the Args is exactly never (function call without any params)
1: IsAny<Schema> extends true
? any
: IsNever<Args> extends true
? ExtractExactFunction<Schema['Functions'][FnName], Args>
: // Otherwise, we attempt to match with one of the function definition in the union based
// on the function arguments provided
Args extends GenericFunction['Args']
? LastOf<FindMatchingFunctionByArgs<Schema['Functions'][FnName], Args>>
: // If we can't find a matching function by args, we try to find one by function name
ExtractExactFunction<Schema['Functions'][FnName], Args> extends GenericFunction
? ExtractExactFunction<Schema['Functions'][FnName], Args>
: any
}[1] extends infer Fn
? // If we are dealing with an non-typed client everything is any
IsAny<Fn> extends true
? { Row: any; Result: any; RelationName: FnName; Relationships: null }
: // Otherwise, we use the arguments based function definition narrowing to get the rigt value
Fn extends GenericFunction
? {
Row: Fn['Returns'] extends any[]
? Fn['Returns'][number] extends Record<string, unknown>
? Fn['Returns'][number]
: never
: Fn['Returns'] extends Record<string, unknown>
? Fn['Returns']
: never
Result: Fn['SetofOptions'] extends GenericSetofOption
? Fn['SetofOptions']['isSetofReturn'] extends true
? Fn['SetofOptions']['isOneToOne'] extends true
? Fn['Returns'][]
: Fn['Returns']
: Fn['Returns']
: Fn['Returns']
RelationName: Fn['SetofOptions'] extends GenericSetofOption
? Fn['SetofOptions']['to']
: FnName
Relationships: Fn['SetofOptions'] extends GenericSetofOption
? Fn['SetofOptions']['to'] extends keyof Schema['Tables']
? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships']
: Schema['Views'][Fn['SetofOptions']['to']]['Relationships']
: null
}
: // If we failed to find the function by argument, we still pass with any but also add an overridable
Fn extends false
? {
Row: any
Result: { error: true } & "Couldn't infer function definition matching provided arguments"
RelationName: FnName
Relationships: null
}
: never
: never

/**
* Response format
*
Expand Down Expand Up @@ -60,9 +133,18 @@ export type GenericNonUpdatableView = {

export type GenericView = GenericUpdatableView | GenericNonUpdatableView

export type GenericSetofOption = {
isSetofReturn?: boolean | undefined
isOneToOne?: boolean | undefined
isNotNullable?: boolean | undefined
to: string
from: string
}

export type GenericFunction = {
Args: Record<string, unknown>
Args: Record<string, unknown> | never
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment

The generated function now use the never as argument which allows to distinguish between "no params at all" and "param with empty name".

That should still be retro-compatible since Record<PropertyKey, never> should be considered as Record<string, unknown>.

Returns: unknown
SetofOptions?: GenericSetofOption
}

export type GenericSchema = {
Expand Down
Loading
Loading