Skip to content

feat(types): add inference for embeded joins by functions #614

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 15 commits into
base: chore/add-auto-types-gen-and-override-for-testing
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"test:types:watch": "run-s build && tsd --files 'test/**/*.test-d.ts' --watch",
"db:clean": "cd test/db && docker compose down --volumes",
"db:run": "cd test/db && docker compose up --detach && wait-for-localhost 3000",
"db:generate-test-types": "cd test/db && docker compose up --detach && wait-for-localhost 8080 && curl --location 'http://0.0.0.0:8080/generators/typescript?included_schemas=public,personal&detect_one_to_one_relationships=true' > ../types.generated.ts && sed -i 's/export type Json = .*/export type Json = unknown;/' ../types.generated.ts"
"db:generate-test-types": "cd test/db && docker compose up --detach && wait-for-localhost 8080 && curl --location 'http://0.0.0.0:8080/generators/typescript?included_schemas=public,personal&detect_one_to_one_relationships=true' > ../types.generated.ts && npm run format"
},
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
Expand Down
89 changes: 77 additions & 12 deletions src/PostgrestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,68 @@ import PostgrestQueryBuilder from './PostgrestQueryBuilder'
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
import PostgrestBuilder from './PostgrestBuilder'
import { DEFAULT_HEADERS } from './constants'
import { Fetch, GenericSchema } from './types'
import { Fetch, GenericFunction, GenericSchema, GenericSetofOption } from './types'
import { FindMatchingFunctionByArgs, IsAny } from './select-query-parser/utils'

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

export type GetRpcFunctionFilterBuilderByArgs<
Schema extends GenericSchema,
FnName extends string & keyof Schema['Functions'],
Args
> = {
0: Schema['Functions'][FnName]
// This is here to handle the case where the args is exactly {} and fallback to the empty
// args function definition if there is one in such case
1: [keyof Args] extends [never]
? 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']
? FindMatchingFunctionByArgs<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['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

/**
* PostgREST client.
Expand Down Expand Up @@ -121,9 +182,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'] = {},
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 @@ -135,14 +204,10 @@ export default class PostgrestClient<
} = {}
): PostgrestFilterBuilder<
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']
> {
let method: 'HEAD' | 'GET' | 'POST'
const url = new URL(`${this.url}/rpc/${fn}`)
Expand Down Expand Up @@ -176,6 +241,6 @@ export default class PostgrestClient<
body,
fetch: this.fetch,
allowEmpty: false,
} as unknown as PostgrestBuilder<Fn['Returns']>)
} as unknown as PostgrestBuilder<FilterBuilder['Row']>)
}
}
5 changes: 3 additions & 2 deletions src/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import PostgrestBuilder from './PostgrestBuilder'
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
import { GetResult } from './select-query-parser/result'
import { GenericSchema, CheckMatchingArrayTypes } from './types'

Expand All @@ -23,7 +24,7 @@ export default class PostgrestTransformBuilder<
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query>
>(
columns?: Query
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], RelationName, Relationships> {
): PostgrestFilterBuilder<Schema, Row, NewResultOne[], RelationName, Relationships> {
Copy link
Member Author

Choose a reason for hiding this comment

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

note

This should fix: supabase/supabase-js#1365

// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = (columns ?? '*')
Expand All @@ -43,7 +44,7 @@ export default class PostgrestTransformBuilder<
this.headers['Prefer'] += ','
}
this.headers['Prefer'] += 'return=representation'
return this as unknown as PostgrestTransformBuilder<
return this as unknown as PostgrestFilterBuilder<
Schema,
Row,
NewResultOne[],
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export type {
// https://github.com/supabase/postgrest-js/issues/551
// To be replaced with a helper type that only uses public types
export type { GetResult as UnstableGetResult } from './select-query-parser/result'
export type { GetRpcFunctionFilterBuilderByArgs } from './PostgrestClient'
13 changes: 10 additions & 3 deletions src/select-query-parser/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,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<Schema, Resolved, Field, CurrentTableOrView>
Expand All @@ -328,7 +328,10 @@ 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
}
direction: string
},
Field extends Ast.FieldNode,
Expand All @@ -351,7 +354,11 @@ type ProcessEmbeddedResourceResult<
? ProcessedChildren
: ProcessedChildren[]
: Resolved['relation']['isOneToOne'] extends true
? ProcessedChildren | null
? Resolved['relation']['match'] extends 'func'
? Resolved['relation']['isNotNullable'] extends true
? ProcessedChildren
: 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
85 changes: 85 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,33 @@ 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
: false
}
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 +523,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 @@ -578,3 +619,47 @@ export type IsStringUnion<T> = string extends T
? false
: 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
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,17 @@ export type GenericNonUpdatableView = {

export type GenericView = GenericUpdatableView | GenericNonUpdatableView

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

export type GenericFunction = {
Args: Record<string, unknown>
Returns: unknown
SetofOptions?: GenericSetofOption
}

export type GenericSchema = {
Expand Down
Loading
Loading