diff --git a/package.json b/package.json index 2942deeb..11f60830 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index 8a37b09c..cfde1697 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -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] extends [S] ? ([S] extends [T] ? true : false) : false + +type ExtractExactFunction = Fns extends infer F + ? F extends GenericFunction + ? ExactMatch 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 + : // 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 + : any +}[1] extends infer Fn + ? // If we are dealing with an non-typed client everything is any + IsAny 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 + ? Fn['Returns'][number] + : never + : Fn['Returns'] extends Record + ? 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. @@ -121,9 +182,17 @@ export default class PostgrestClient< * `"estimated"`: Uses exact count for low numbers and planned count for high * numbers. */ - rpc( + rpc< + FnName extends string & keyof Schema['Functions'], + Args extends Schema['Functions'][FnName]['Args'] = {}, + FilterBuilder extends GetRpcFunctionFilterBuilderByArgs< + Schema, + FnName, + Args + > = GetRpcFunctionFilterBuilderByArgs + >( fn: FnName, - args: Fn['Args'] = {}, + args: Args = {} as Args, { head = false, get = false, @@ -135,14 +204,10 @@ export default class PostgrestClient< } = {} ): PostgrestFilterBuilder< Schema, - Fn['Returns'] extends any[] - ? Fn['Returns'][number] extends Record - ? 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}`) @@ -176,6 +241,6 @@ export default class PostgrestClient< body, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } } diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index c9fb5781..14710fdc 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -1,4 +1,5 @@ import PostgrestBuilder from './PostgrestBuilder' +import PostgrestFilterBuilder from './PostgrestFilterBuilder' import { GetResult } from './select-query-parser/result' import { GenericSchema, CheckMatchingArrayTypes } from './types' @@ -23,7 +24,7 @@ export default class PostgrestTransformBuilder< NewResultOne = GetResult >( columns?: Query - ): PostgrestTransformBuilder { + ): PostgrestFilterBuilder { // Remove whitespaces except when quoted let quoted = false const cleanedColumns = (columns ?? '*') @@ -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[], diff --git a/src/index.ts b/src/index.ts index 466a71bf..c4f84067 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index a32567c2..de1bb677 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -310,7 +310,7 @@ export type ProcessEmbeddedResource< > = ResolveRelationship extends infer Resolved ? Resolved extends { referencedTable: Pick - relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } + relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' } direction: string } ? ProcessEmbeddedResourceResult @@ -328,7 +328,10 @@ type ProcessEmbeddedResourceResult< Schema extends GenericSchema, Resolved extends { referencedTable: Pick - relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } + relation: GenericRelationship & { + match: 'refrel' | 'col' | 'fkname' | 'func' + isNotNullable?: boolean + } direction: string }, Field extends Ast.FieldNode, @@ -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 diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index bcbfef04..b0fb592c 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -1,3 +1,4 @@ +import { GenericFunction, GenericSetofOption } from '../types' import { Ast } from './parser' import { AggregateFunctions, @@ -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[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']}`> @@ -495,6 +523,19 @@ type ResolveJoinTableRelationship< : never }[keyof TablesAndViews] +type ResolveEmbededFunctionJoinTableRelationship< + Schema extends GenericSchema, + CurrentTableOrView extends keyof TablesAndViews & 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 & string, @@ -578,3 +619,47 @@ export type IsStringUnion = string extends T ? false : true : false + +// Functions matching utils +export type IsMatchingArgs< + FnArgs extends GenericFunction['Args'], + PassedArgs extends GenericFunction['Args'] +> = [FnArgs] extends [Record] + ? PassedArgs extends Record + ? 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 extends true + ? Fn + : never + : never + +export type FindMatchingFunctionByArgs< + FnUnion, + Args extends GenericFunction['Args'] +> = FnUnion extends infer Fn extends GenericFunction ? MatchingFunctionArgs : 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 + : false diff --git a/src/types.ts b/src/types.ts index 51d58a70..5b09427f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 Returns: unknown + SetofOptions?: GenericSetofOption } export type GenericSchema = { diff --git a/test/advanced_rpc.test-d.ts b/test/advanced_rpc.test-d.ts new file mode 100644 index 00000000..c03b0855 --- /dev/null +++ b/test/advanced_rpc.test-d.ts @@ -0,0 +1,322 @@ +import { expectType } from 'tsd' +import { TypeEqual } from 'ts-expect' +import { Database } from './types.override' +import { rpcQueries } from './advanced_rpc' +import { SelectQueryError } from '../src/select-query-parser/utils' + +type Schema = Database['public'] + +{ + const { data } = await rpcQueries['function returning a setof embeded table'] + let result: Exclude + let expected: Array> + expectType>(true) +} + +{ + const { data } = await rpcQueries['function double definition returning a setof embeded table'] + let result: Exclude + let expected: Array> + expectType>(true) +} + +{ + const { data } = await rpcQueries['function returning a single row embeded table'] + let result: Exclude + let expected: Schema['Tables']['user_profiles']['Row'] + expectType>(true) +} + +{ + const { data } = await rpcQueries['function with scalar input'] + let result: Exclude + let expected: Array> + expectType>(true) +} + +{ + const { data } = await rpcQueries['function with table row input'] + let result: Exclude + let expected: Array> + expectType>(true) +} + +{ + const { data } = await rpcQueries['function with view row input'] + let result: Exclude + let expected: Array> + expectType>(true) +} + +{ + const { data } = await rpcQueries['function returning view'] + let result: Exclude + let expected: Array + expectType>(true) +} + +{ + const { data } = await rpcQueries['function with scalar input returning view'] + let result: Exclude + let expected: Array + expectType>(true) +} + +{ + const { data } = await rpcQueries['function with scalar input with followup select'] + let result: Exclude + let expected: Array<{ + channel_id: number | null + message: string | null + users: { + catchphrase: unknown + username: string + } | null + }> + expectType>(true) +} + +{ + const { data } = await rpcQueries['function with row input with followup select'] + let result: Exclude + let expected: Array<{ + id: number + username: string | null + users: { + catchphrase: unknown + username: string + } | null + }> + expectType>(true) +} + +// Tests for unresolvable functions +{ + const { data } = await rpcQueries['unresolvable function with no params'] + let result: Exclude + let expected: undefined + expectType>(true) +} + +{ + const { data } = await rpcQueries['unresolvable function with text param'] + let result: Exclude + // Should be an error response due to ambiguous function resolution + let expected: SelectQueryError<'Could not choose the best candidate function between: postgrest_unresolvable_function(a => int4), postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + expectType>(true) +} + +{ + const { data } = await rpcQueries['unresolvable function with int param'] + let result: Exclude + // Should be an error response due to ambiguous function resolution + let expected: SelectQueryError<'Could not choose the best candidate function between: postgrest_unresolvable_function(a => int4), postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + expectType>(true) +} + +// Tests for resolvable functions +{ + const { data } = await rpcQueries['resolvable function with no params'] + let result: Exclude + let expected: undefined + expectType>(true) +} + +{ + const { data } = await rpcQueries['resolvable function with text param'] + let result: Exclude + let expected: number + expectType>(true) +} + +{ + const { data } = await rpcQueries['resolvable function with int param'] + let result: Exclude + let expected: string + expectType>(true) +} + +{ + const { data } = await rpcQueries['resolvable function with profile_id param'] + let result: Exclude + let expected: Array + expectType>(true) +} + +{ + const { data } = await rpcQueries['resolvable function with channel_id and search params'] + let result: Exclude + let expected: Array> + expectType>(true) +} + +{ + const { data } = await rpcQueries['resolvable function with user_row param'] + let result: Exclude + let expected: Array> + expectType>(true) +} + +// Tests for polymorphic functions +{ + const { data } = await rpcQueries['polymorphic function with text param'] + let result: Exclude + let expected: string + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed json param'] + let result: Exclude + let expected: number + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed jsonb param'] + let result: Exclude + let expected: number + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed text param'] + let result: Exclude + let expected: number + expectType>(true) +} + +{ + const { data } = await rpcQueries[ + 'polymorphic function with unnamed params definition call with bool param' + ] + let result: Exclude + // TODO: since this call use an invalid type definition, we can't distinguish between the "no values" or the "empty" + // property call: + // polymorphic_function_with_no_params_or_unnamed: + // | { + // Args: Record + // Returns: number + // } + // | { + // Args: { '': string } + // Returns: string + // } + // A type error would be raised at higher level (argument providing) time though + let expected: string | number + expectType>(true) +} + +{ + // The same call, but using a valid text params: `{'': 'test'}` should properly + // narrow down the resilt to the right definition + const { data } = await rpcQueries[ + 'polymorphic function with unnamed params definition call with text param' + ] + let result: Exclude + let expected: string + expectType>(true) +} +{ + // Same thing if we call the function with explicitely no params, it should narrow down to the right definition + const { data } = await rpcQueries[ + 'polymorphic function with no params and unnamed params definition call with no params' + ] + let result: Exclude + let expected: number + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed default no params'] + let result: Exclude + let expected: SelectQueryError<'Could not choose the best candidate function between: polymorphic_function_with_unnamed_default( => int4), polymorphic_function_with_unnamed_default(). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed default int param'] + let result: Exclude + // TODO: since this call use an invalid type definition, we can't distinguish between the "no values" or the "empty" + // A type error would be raised at higher level (argument providing) time though + let expected: + | string + | SelectQueryError<'Could not choose the best candidate function between: polymorphic_function_with_unnamed_default( => int4), polymorphic_function_with_unnamed_default(). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed default text param'] + let result: Exclude + let expected: string + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed default overload no params'] + let result: Exclude + let expected: SelectQueryError<'Could not choose the best candidate function between: polymorphic_function_with_unnamed_default_overload( => int4), polymorphic_function_with_unnamed_default_overload(). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed default overload int param'] + let result: Exclude + // TODO: since this call use an invalid type definition, we can't distinguish between the "no values" or the "empty" + // A type error would be raised at higher level (argument providing) time though + let expected: + | string + | SelectQueryError<'Could not choose the best candidate function between: polymorphic_function_with_unnamed_default_overload( => int4), polymorphic_function_with_unnamed_default_overload(). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed default overload text param'] + let result: Exclude + let expected: string + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed default overload bool param'] + let result: Exclude + // TODO: since this call use an invalid type definition, we can't distinguish between the "no values" or the "empty" + // A type error would be raised at higher level (argument providing) time though + let expected: + | string + | SelectQueryError<'Could not choose the best candidate function between: polymorphic_function_with_unnamed_default_overload( => int4), polymorphic_function_with_unnamed_default_overload(). Try renaming the parameters or the function itself in the database so function overloading can be resolved'> + expectType>(true) +} + +{ + const { data } = await rpcQueries['function with blurb_message'] + let result: Exclude + let expected: never + expectType>(true) +} + +{ + const { data } = await rpcQueries['function returning row'] + let result: Exclude + let expected: { + age_range: unknown + catchphrase: unknown + data: unknown + status: 'ONLINE' | 'OFFLINE' | null + username: string + } + expectType>(true) +} + +{ + const { data } = await rpcQueries['function returning set of rows'] + let result: Exclude + let expected: Array<{ + age_range: unknown + catchphrase: unknown + data: unknown + status: 'ONLINE' | 'OFFLINE' | null + username: string + }> + expectType>(true) +} diff --git a/test/advanced_rpc.ts b/test/advanced_rpc.ts new file mode 100644 index 00000000..d7249356 --- /dev/null +++ b/test/advanced_rpc.ts @@ -0,0 +1,1027 @@ +import { PostgrestClient } from '../src/index' +import { Database } from './types.override' + +const REST_URL = 'http://localhost:3000' +export const postgrest = new PostgrestClient(REST_URL) + +export const rpcQueries = { + 'function returning a setof embeded table': postgrest.rpc('get_messages', { + channel_row: { id: 1, data: null, slug: null }, + }), + 'function double definition returning a setof embeded table': postgrest.rpc('get_messages', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }), + 'function returning a single row embeded table': postgrest.rpc('get_user_profile', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }), + 'function with scalar input': postgrest.rpc('get_messages_by_username', { + search_username: 'supabot', + }), + 'function with table row input': postgrest.rpc('get_user_messages', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }), + 'function with view row input': postgrest.rpc('get_active_user_messages', { + active_user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }), + 'function returning view': postgrest.rpc('get_user_recent_messages', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }), + 'function with scalar input returning view': postgrest.rpc('get_recent_messages_by_username', { + search_username: 'supabot', + }), + 'function with scalar input with followup select': postgrest + .rpc('get_recent_messages_by_username', { + search_username: 'supabot', + }) + .select('channel_id, message, users(username, catchphrase)'), + 'function with row input with followup select': postgrest + .rpc('get_user_profile', { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + }) + .select('id, username, users(username, catchphrase)'), + 'unresolvable function with no params': postgrest.rpc('postgrest_unresolvable_function'), + 'unresolvable function with text param': postgrest.rpc('postgrest_unresolvable_function', { + a: 'test', + }), + 'unresolvable function with int param': postgrest.rpc('postgrest_unresolvable_function', { + a: 1, + }), + 'resolvable function with no params': postgrest.rpc( + 'postgrest_resolvable_with_override_function' + ), + 'resolvable function with text param': postgrest.rpc( + 'postgrest_resolvable_with_override_function', + { + a: 'test', + } + ), + 'resolvable function with int param': postgrest.rpc( + 'postgrest_resolvable_with_override_function', + { + b: 1, + } + ), + 'resolvable function with profile_id param': postgrest.rpc( + 'postgrest_resolvable_with_override_function', + { + profile_id: 1, + } + ), + 'resolvable function with channel_id and search params': postgrest.rpc( + 'postgrest_resolvable_with_override_function', + { + cid: 1, + search: 'Hello World 👋', + } + ), + 'resolvable function with user_row param': postgrest.rpc( + 'postgrest_resolvable_with_override_function', + { + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, + } + ), + 'polymorphic function with text param': postgrest.rpc( + 'polymorphic_function_with_different_return', + { + '': 'test', + } + ), + 'polymorphic function with bool param': postgrest.rpc( + 'polymorphic_function_with_different_return', + { + // @ts-expect-error should not have a function with a single unnamed params that isn't json/jsonb/text in types definitions + '': true, + } + ), + 'polymorphic function with unnamed int param': postgrest.rpc( + // @ts-expect-error should not have a function with a single unnamed params that isn't json/jsonb/text in types definitions + 'polymorphic_function_with_unnamed_integer', + { + '': 1, + } + ), + 'polymorphic function with unnamed json param': postgrest.rpc( + 'polymorphic_function_with_unnamed_json', + { + '': { test: 'value' }, + } + ), + 'polymorphic function with unnamed jsonb param': postgrest.rpc( + 'polymorphic_function_with_unnamed_jsonb', + { + '': { test: 'value' }, + } + ), + 'polymorphic function with unnamed text param': postgrest.rpc( + 'polymorphic_function_with_unnamed_text', + { + '': 'test', + } + ), + 'polymorphic function with no params and unnamed params definition call with no params': + postgrest.rpc('polymorphic_function_with_no_params_or_unnamed'), + 'polymorphic function with unnamed params definition call with bool param': postgrest.rpc( + 'polymorphic_function_with_no_params_or_unnamed', + { + // @ts-expect-error should not have generated a type definition for the boolean + '': true, + } + ), + 'polymorphic function with unnamed params definition call with text param': postgrest.rpc( + 'polymorphic_function_with_no_params_or_unnamed', + { + '': 'test', + } + ), + 'polymorphic function with unnamed default no params': postgrest.rpc( + 'polymorphic_function_with_unnamed_default' + ), + 'polymorphic function with unnamed default int param': postgrest.rpc( + 'polymorphic_function_with_unnamed_default', + { + //@ts-expect-error the type definition for empty params should be text + '': 123, + } + ), + 'polymorphic function with unnamed default text param': postgrest.rpc( + 'polymorphic_function_with_unnamed_default', + { + '': 'custom text', + } + ), + 'polymorphic function with unnamed default overload no params': postgrest.rpc( + 'polymorphic_function_with_unnamed_default_overload' + ), + 'polymorphic function with unnamed default overload int param': postgrest.rpc( + 'polymorphic_function_with_unnamed_default_overload', + { + //@ts-expect-error the type definition for empty params should be text + '': 123, + } + ), + 'polymorphic function with unnamed default overload text param': postgrest.rpc( + 'polymorphic_function_with_unnamed_default_overload', + { + '': 'custom text', + } + ), + 'polymorphic function with unnamed default overload bool param': postgrest.rpc( + 'polymorphic_function_with_unnamed_default_overload', + { + //@ts-expect-error + '': true, + } + ), + // @ts-expect-error the function types doesn't exist and should fail to be retrieved by cache + // for direct rpc call + 'function with blurb_message': postgrest.rpc('blurb_messages', { + channel_id: 1, + data: null, + id: 1, + message: 'Hello World 👋', + username: 'supabot', + }), + 'function returning row': postgrest.rpc('function_returning_row'), + 'function returning set of rows': postgrest.rpc('function_returning_set_of_rows'), +} as const + +describe('rpc', () => { + test('function returning a setof embeded table', async () => { + const res = await rpcQueries['function returning a setof embeded table'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function double definition returning a setof embeded table', async () => { + const res = await rpcQueries['function double definition returning a setof embeded table'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning a single row embeded table', async () => { + const res = await rpcQueries['function returning a single row embeded table'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "id": 1, + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with scalar input', async () => { + const res = await rpcQueries['function with scalar input'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with table row input', async () => { + const res = await rpcQueries['function with table row input'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with view row input', async () => { + const res = await rpcQueries['function with view row input'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning view', async () => { + const res = await rpcQueries['function returning view'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with scalar input returning view', async () => { + const res = await rpcQueries['function with scalar input returning view'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with scalar input with followup select', async () => { + const res = await rpcQueries['function with scalar input with followup select'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 3, + "message": "Some message on channel wihtout details", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + Object { + "channel_id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + Object { + "channel_id": 1, + "message": "Hello World 👋", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('should be able to filter before and after select rpc', async () => { + const res = await postgrest + .rpc('get_user_profile', { + //@ts-expect-error will complain about missing the rest of the params + user_row: { username: 'supabot' }, + }) + .select('id, username, users(username, catchphrase)') + .eq('username', 'nope') + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + const res2 = await postgrest + .rpc('get_user_profile', { + //@ts-expect-error will complain about missing the rest of the params + user_row: { username: 'supabot' }, + }) + // should also be able to fitler before the select + .eq('username', 'nope') + .select('id, username, users(username, catchphrase)') + + expect(res2).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('unresolvable function with text param', async () => { + const res = await rpcQueries['unresolvable function with text param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => integer), public.postgrest_unresolvable_function(a => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('unresolvable function with int param', async () => { + const res = await rpcQueries['unresolvable function with int param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => integer), public.postgrest_unresolvable_function(a => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('resolvable function with no params', async () => { + const res = await rpcQueries['resolvable function with no params'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 204, + "statusText": "No Content", + } + `) + }) + + test('resolvable function with text param', async () => { + const res = await rpcQueries['resolvable function with text param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('resolvable function with int param', async () => { + const res = await rpcQueries['resolvable function with int param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('resolvable function with profile_id param', async () => { + const res = await rpcQueries['resolvable function with profile_id param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "id": 1, + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('resolvable function with channel_id and search params', async () => { + const res = await rpcQueries['resolvable function with channel_id and search params'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('resolvable function with user_row param', async () => { + const res = await rpcQueries['resolvable function with user_row param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with text param', async () => { + const res = await rpcQueries['polymorphic function with text param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with bool param', async () => { + const res = await rpcQueries['polymorphic function with bool param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed int param', async () => { + const res = await rpcQueries['polymorphic function with unnamed int param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST202", + "details": "Searched for the function public.polymorphic_function_with_unnamed_integer with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.", + "hint": "Perhaps you meant to call the function public.polymorphic_function_with_unnamed_text", + "message": "Could not find the function public.polymorphic_function_with_unnamed_integer() in the schema cache", + }, + "status": 404, + "statusText": "Not Found", + } + `) + }) + + test('polymorphic function with unnamed json param', async () => { + const res = await rpcQueries['polymorphic function with unnamed json param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed jsonb param', async () => { + const res = await rpcQueries['polymorphic function with unnamed jsonb param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed text param', async () => { + const res = await rpcQueries['polymorphic function with unnamed text param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with no params and unnamed params definition call with no params', async () => { + const res = await rpcQueries[ + 'polymorphic function with no params and unnamed params definition call with no params' + ] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": 1, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed params definition call with bool param', async () => { + const res = await rpcQueries[ + 'polymorphic function with unnamed params definition call with bool param' + ] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed params definition call with text param', async () => { + const res = await rpcQueries[ + 'polymorphic function with unnamed params definition call with text param' + ] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default no params', async () => { + const res = await rpcQueries['polymorphic function with unnamed default no params'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('polymorphic function with unnamed default int param', async () => { + const res = await rpcQueries['polymorphic function with unnamed default int param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default text param', async () => { + const res = await rpcQueries['polymorphic function with unnamed default text param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default overload no params', async () => { + const res = await rpcQueries['polymorphic function with unnamed default overload no params'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST203", + "details": null, + "hint": "Try renaming the parameters or the function itself in the database so function overloading can be resolved", + "message": "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text)", + }, + "status": 300, + "statusText": "Multiple Choices", + } + `) + }) + + test('polymorphic function with unnamed default overload int param', async () => { + const res = await rpcQueries['polymorphic function with unnamed default overload int param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default overload text param', async () => { + const res = await rpcQueries['polymorphic function with unnamed default overload text param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('polymorphic function with unnamed default overload bool param', async () => { + // TODO: res is an union of types because we can't narrow it with an unused field + const res = await rpcQueries['polymorphic function with unnamed default overload bool param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": "foo", + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with blurb_message', async () => { + const res = await rpcQueries['function with blurb_message'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST202", + "details": "Searched for the function public.blurb_messages with parameters channel_id, data, id, message, username or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.", + "hint": "Perhaps you meant to call the function public.get_messages", + "message": "Could not find the function public.blurb_messages(channel_id, data, id, message, username) in the schema cache", + }, + "status": 404, + "statusText": "Not Found", + } + `) + }) + + test('function returning row', async () => { + const res = await rpcQueries['function returning row'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning set of rows', async () => { + const res = await rpcQueries['function returning set of rows'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'cat'", + "data": null, + "status": "OFFLINE", + "username": "kiwicopple", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "awailas", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'json' 'test'", + "data": Object { + "foo": Object { + "bar": Object { + "nested": "value", + }, + "baz": "string value", + }, + }, + "status": "ONLINE", + "username": "jsonuser", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) +}) diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index ee3f6e2e..7734cee6 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -163,3 +163,168 @@ create table public.cornercase ( "column whitespace" text, array_column text[] ); + +-- Function that returns a single user profile for a user +CREATE OR REPLACE FUNCTION public.get_user_profile(user_row users) +RETURNS SETOF user_profiles +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM public.user_profiles WHERE username = user_row.username; +$$; + +-- Same definition, but will be used with a type override to pretend this can't ever return null +CREATE OR REPLACE FUNCTION public.get_user_profile_non_nullable(user_row users) +RETURNS SETOF user_profiles +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM public.user_profiles WHERE username = user_row.username; +$$; + + +CREATE OR REPLACE FUNCTION public.get_messages(channel_row channels) +RETURNS SETOF messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.messages WHERE channel_id = channel_row.id; +$$; + +CREATE OR REPLACE FUNCTION public.get_messages(user_row users) +RETURNS SETOF messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.messages WHERE username = user_row.username; +$$; + + +-- Create a view based on users table +CREATE VIEW public.active_users AS + SELECT * FROM public.users WHERE status = 'ONLINE'::public.user_status; + +-- Create a view based on messages table +CREATE VIEW public.recent_messages AS + SELECT * FROM public.messages ORDER BY id DESC LIMIT 100; + +-- Function returning messages using scalar as input (username) +CREATE OR REPLACE FUNCTION public.get_messages_by_username(search_username text) +RETURNS SETOF messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.messages WHERE username = search_username; +$$; + +-- Function returning messages using table row as input +CREATE OR REPLACE FUNCTION public.get_user_messages(user_row users) +RETURNS SETOF messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.messages WHERE username = user_row.username; +$$; + +-- Function returning messages using view row as input +CREATE OR REPLACE FUNCTION public.get_active_user_messages(active_user_row active_users) +RETURNS SETOF messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.messages WHERE username = active_user_row.username; +$$; + +-- Function returning view using scalar as input +CREATE OR REPLACE FUNCTION public.get_recent_messages_by_username(search_username text) +RETURNS SETOF recent_messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.recent_messages WHERE username = search_username; +$$; + +-- Function returning view using table row as input +CREATE OR REPLACE FUNCTION public.get_user_recent_messages(user_row users) +RETURNS SETOF recent_messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.recent_messages WHERE username = user_row.username; +$$; +CREATE OR REPLACE FUNCTION public.get_user_recent_messages(active_user_row active_users) +RETURNS SETOF recent_messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.recent_messages WHERE username = active_user_row.username; +$$; +CREATE OR REPLACE FUNCTION public.get_user_first_message(active_user_row active_users) +RETURNS SETOF recent_messages ROWS 1 +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.recent_messages WHERE username = active_user_row.username ORDER BY id ASC LIMIT 1; +$$; + + +-- Valid postgresql function override but that produce an unresolvable postgrest function call +create function postgrest_unresolvable_function() returns void language sql as ''; +create function postgrest_unresolvable_function(a text) returns int language sql as 'select 1'; +create function postgrest_unresolvable_function(a int) returns text language sql as $$ + SELECT 'foo' +$$; +-- Valid postgresql function override with differents returns types depending of different arguments +create function postgrest_resolvable_with_override_function() returns void language sql as ''; +create function postgrest_resolvable_with_override_function(a text) returns int language sql as 'select 1'; +create function postgrest_resolvable_with_override_function(b int) returns text language sql as $$ + SELECT 'foo' +$$; +-- Function overrides returning setof tables +create function postgrest_resolvable_with_override_function(profile_id bigint) returns setof user_profiles language sql stable as $$ + SELECT * FROM user_profiles WHERE id = profile_id; +$$; +create function postgrest_resolvable_with_override_function(cid bigint, search text default '') returns setof messages language sql stable as $$ + SELECT * FROM messages WHERE channel_id = cid AND message = search; +$$; +-- Function override taking a table as argument and returning a setof +create function postgrest_resolvable_with_override_function(user_row users) returns setof messages language sql stable as $$ + SELECT * FROM messages WHERE messages.username = user_row.username; +$$; + +create or replace function public.polymorphic_function_with_different_return(bool) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_different_return(int) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_different_return(text) returns text language sql as $$ SELECT 'foo' $$; + +create or replace function public.polymorphic_function_with_no_params_or_unnamed() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_no_params_or_unnamed(bool) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_no_params_or_unnamed(text) returns text language sql as $$ SELECT 'foo' $$; +-- Function with a single unnamed params that isn't a json/jsonb/text should never appears in the type gen as it won't be in postgrest schema +create or replace function public.polymorphic_function_with_unnamed_integer(int) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_json(json) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_jsonb(jsonb) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_text(text) returns int language sql as 'SELECT 1'; + +-- Functions with unnamed parameters that have default values +create or replace function public.polymorphic_function_with_unnamed_default() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_default(int default 42) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_unnamed_default(text default 'default') returns text language sql as $$ SELECT 'foo' $$; + +-- Functions with unnamed parameters that have default values and multiple overloads +create or replace function public.polymorphic_function_with_unnamed_default_overload() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_default_overload(int default 42) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_unnamed_default_overload(text default 'default') returns text language sql as $$ SELECT 'foo' $$; +create or replace function public.polymorphic_function_with_unnamed_default_overload(bool default true) returns int language sql as 'SELECT 3'; + +create function public.blurb_message(public.messages) returns character varying as +$$ +select substring($1.message, 1, 3); +$$ language sql stable; + + +create or replace function public.function_returning_row() +returns public.users +language sql +stable +as $$ + select * from public.users limit 1; +$$; + +create or replace function public.function_returning_set_of_rows() +returns setof public.users +language sql +stable +as $$ + select * from public.users; +$$; diff --git a/test/db/docker-compose.yml b/test/db/docker-compose.yml index c2f91a1d..d6ed77d7 100644 --- a/test/db/docker-compose.yml +++ b/test/db/docker-compose.yml @@ -1,6 +1,5 @@ # docker-compose.yml -version: '3' services: rest: image: postgrest/postgrest:v12.2.0 @@ -29,7 +28,7 @@ services: POSTGRES_HOST: /var/run/postgresql POSTGRES_PORT: 5432 pgmeta: - image: supabase/postgres-meta:v0.87.1 + image: local-pg-meta ports: - '8080:8080' environment: diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts new file mode 100644 index 00000000..23d56f0f --- /dev/null +++ b/test/embeded_functions_join.test-d.ts @@ -0,0 +1,159 @@ +import { expectType } from 'tsd' +import { TypeEqual } from 'ts-expect' +import { Database } from './types.override' +import { selectQueries } from './embeded_functions_join' + +type Schema = Database['public'] + +{ + const { data } = await selectQueries.embeded_setof_function + let result: Exclude + let expected: Array<{ + id: number + all_channels_messages: Array + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_function_fields_selection + let result: Exclude + let expected: Array<{ + id: number + all_channels_messages: Array> + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_function_double_definition + let result: Exclude + let expected: Array<{ + username: string + all_user_messages: Array + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_function_double_definition_fields_selection + let result: Exclude + let expected: Array<{ + username: string + all_user_messages: Array> + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_row_one_function + let result: Exclude + let expected: Array<{ + username: string + user_called_profile: Schema['Tables']['user_profiles']['Row'] | null + }> + expectType>(true) +} +{ + const { data } = await selectQueries.embeded_setof_row_one_function_not_nullable + let result: Exclude + let expected: Array<{ + username: string + user_called_profile_not_null: Schema['Tables']['user_profiles']['Row'] + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_row_one_function_with_fields_selection + let result: Exclude + let expected: Array<{ + username: string + user_called_profile: Pick | null + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_function_with_fields_selection_with_sub_linking + let result: Exclude + let expected: Array<{ + id: number + all_channels_messages: Array<{ + id: number + message: string | null + channels: { + id: number + slug: string | null + } + }> + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_function_with_table_row_input + let result: Exclude + let expected: Array<{ + username: string + user_messages: Array + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_function_with_view_row_input + let result: Exclude + let expected: Array<{ + username: string | null + active_user_messages: Array + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_function_returning_view + let result: Exclude + let expected: Array<{ + username: string + recent_messages: Array + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_function_with_view_input_returning_view + let result: Exclude + let expected: Array<{ + username: string | null + recent_messages: Array + }> + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_function_with_blurb_message + let result: Exclude + let expected: Array<{ + username: string + user_messages: Array< + Pick + > + }> + expectType>(true) +} + +// Cannot embed an function that is not a setofOptions one +{ + const { data } = await selectQueries.embeded_function_returning_row + let result: Exclude + let expected: never[] + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_function_returning_set_of_rows + let result: Exclude + let expected: never[] + expectType>(true) +} diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts new file mode 100644 index 00000000..93b206dd --- /dev/null +++ b/test/embeded_functions_join.ts @@ -0,0 +1,737 @@ +import { PostgrestClient } from '../src/index' +import { Database } from './types.override' + +const REST_URL = 'http://localhost:3000' +export const postgrest = new PostgrestClient(REST_URL) + +export const selectParams = { + embeded_setof_function: { from: 'channels', select: 'id, all_channels_messages:get_messages(*)' }, + embeded_setof_function_fields_selection: { + from: 'channels', + select: 'id, all_channels_messages:get_messages(id,message)', + }, + embeded_setof_function_double_definition: { + from: 'users', + select: 'username, all_user_messages:get_messages(*)', + }, + embeded_setof_function_double_definition_fields_selection: { + from: 'users', + select: 'username, all_user_messages:get_messages(id,message)', + }, + embeded_setof_row_one_function: { + from: 'users', + select: 'username, user_called_profile:get_user_profile(*)', + }, + embeded_setof_row_one_function_not_nullable: { + from: 'users', + select: 'username, user_called_profile_not_null:get_user_profile_non_nullable(*)', + }, + embeded_setof_row_one_function_with_fields_selection: { + from: 'users', + select: 'username, user_called_profile:get_user_profile(username)', + }, + embeded_setof_function_with_fields_selection_with_sub_linking: { + from: 'channels', + select: 'id, all_channels_messages:get_messages(id,message,channels(id,slug))', + }, + embeded_function_with_table_row_input: { + from: 'users', + select: 'username, user_messages:get_user_messages(*)', + }, + embeded_function_with_view_row_input: { + from: 'active_users', + select: 'username, active_user_messages:get_active_user_messages(*)', + }, + embeded_function_returning_view: { + from: 'users', + select: 'username, recent_messages:get_user_recent_messages(*)', + }, + embeded_function_with_view_input_returning_view: { + from: 'active_users', + select: 'username, recent_messages:get_user_recent_messages(*)', + }, + embeded_function_with_blurb_message: { + from: 'users', + select: 'username, user_messages:get_user_messages(id,message,blurb_message)', + }, + embeded_function_returning_row: { + from: 'channels', + select: 'id, user:function_returning_row(*)', + }, + embeded_function_returning_set_of_rows: { + from: 'channels', + select: 'id, users:function_returning_set_of_rows(*)', + }, +} as const + +export const selectQueries = { + embeded_setof_function: postgrest + .from(selectParams.embeded_setof_function.from) + .select(selectParams.embeded_setof_function.select), + embeded_setof_function_fields_selection: postgrest + .from(selectParams.embeded_setof_function_fields_selection.from) + .select(selectParams.embeded_setof_function_fields_selection.select), + embeded_setof_function_double_definition: postgrest + .from(selectParams.embeded_setof_function_double_definition.from) + .select(selectParams.embeded_setof_function_double_definition.select), + embeded_setof_function_double_definition_fields_selection: postgrest + .from(selectParams.embeded_setof_function_double_definition_fields_selection.from) + .select(selectParams.embeded_setof_function_double_definition_fields_selection.select), + embeded_setof_row_one_function: postgrest + .from(selectParams.embeded_setof_row_one_function.from) + .select(selectParams.embeded_setof_row_one_function.select), + embeded_setof_row_one_function_not_nullable: postgrest + .from(selectParams.embeded_setof_row_one_function_not_nullable.from) + .select(selectParams.embeded_setof_row_one_function_not_nullable.select), + embeded_setof_row_one_function_with_fields_selection: postgrest + .from(selectParams.embeded_setof_row_one_function_with_fields_selection.from) + .select(selectParams.embeded_setof_row_one_function_with_fields_selection.select), + embeded_setof_function_with_fields_selection_with_sub_linking: postgrest + .from(selectParams.embeded_setof_function_with_fields_selection_with_sub_linking.from) + .select(selectParams.embeded_setof_function_with_fields_selection_with_sub_linking.select), + embeded_function_with_table_row_input: postgrest + .from(selectParams.embeded_function_with_table_row_input.from) + .select(selectParams.embeded_function_with_table_row_input.select), + embeded_function_with_view_row_input: postgrest + .from(selectParams.embeded_function_with_view_row_input.from) + .select(selectParams.embeded_function_with_view_row_input.select), + embeded_function_returning_view: postgrest + .from(selectParams.embeded_function_returning_view.from) + .select(selectParams.embeded_function_returning_view.select), + embeded_function_with_view_input_returning_view: postgrest + .from(selectParams.embeded_function_with_view_input_returning_view.from) + .select(selectParams.embeded_function_with_view_input_returning_view.select), + embeded_function_with_blurb_message: postgrest + .from(selectParams.embeded_function_with_blurb_message.from) + .select(selectParams.embeded_function_with_blurb_message.select), + embeded_function_returning_row: postgrest + .from(selectParams.embeded_function_returning_row.from) + .select(selectParams.embeded_function_returning_row.select), + embeded_function_returning_set_of_rows: postgrest + .from(selectParams.embeded_function_returning_set_of_rows.from) + .select(selectParams.embeded_function_returning_set_of_rows.select), +} as const + +describe('select', () => { + test('function returning a setof embeded table', async () => { + const res = await selectQueries.embeded_setof_function + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_channels_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "id": 1, + }, + Object { + "all_channels_messages": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + ], + "id": 2, + }, + Object { + "all_channels_messages": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "id": 3, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning a setof embeded table with fields selection', async () => { + const res = await selectQueries.embeded_setof_function_fields_selection + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_channels_messages": Array [ + Object { + "id": 1, + "message": "Hello World 👋", + }, + ], + "id": 1, + }, + Object { + "all_channels_messages": Array [ + Object { + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + ], + "id": 2, + }, + Object { + "all_channels_messages": Array [ + Object { + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "id": 3, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function double definition returning a setof embeded table', async () => { + const res = await selectQueries.embeded_setof_function_double_definition + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_user_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "all_user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "all_user_messages": Array [], + "username": "awailas", + }, + Object { + "all_user_messages": Array [], + "username": "jsonuser", + }, + Object { + "all_user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function double definition returning a setof embeded table with fields selection', async () => { + const res = await selectQueries.embeded_setof_function_double_definition_fields_selection + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_user_messages": Array [ + Object { + "id": 1, + "message": "Hello World 👋", + }, + Object { + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + Object { + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "username": "supabot", + }, + Object { + "all_user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "all_user_messages": Array [], + "username": "awailas", + }, + Object { + "all_user_messages": Array [], + "username": "jsonuser", + }, + Object { + "all_user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning a single row embeded table', async () => { + const res = await selectQueries.embeded_setof_row_one_function + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_called_profile": Object { + "id": 1, + "username": "supabot", + }, + "username": "supabot", + }, + Object { + "user_called_profile": null, + "username": "kiwicopple", + }, + Object { + "user_called_profile": null, + "username": "awailas", + }, + Object { + "user_called_profile": null, + "username": "jsonuser", + }, + Object { + "user_called_profile": null, + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning a single row embeded table with fields selection', async () => { + const res = await selectQueries.embeded_setof_row_one_function_with_fields_selection + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_called_profile": Object { + "username": "supabot", + }, + "username": "supabot", + }, + Object { + "user_called_profile": null, + "username": "kiwicopple", + }, + Object { + "user_called_profile": null, + "username": "awailas", + }, + Object { + "user_called_profile": null, + "username": "jsonuser", + }, + Object { + "user_called_profile": null, + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function embedded table with fields selection and sub linking', async () => { + const res = await selectQueries.embeded_setof_function_with_fields_selection_with_sub_linking + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "all_channels_messages": Array [ + Object { + "channels": Object { + "id": 1, + "slug": "public", + }, + "id": 1, + "message": "Hello World 👋", + }, + ], + "id": 1, + }, + Object { + "all_channels_messages": Array [ + Object { + "channels": Object { + "id": 2, + "slug": "random", + }, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + ], + "id": 2, + }, + Object { + "all_channels_messages": Array [ + Object { + "channels": Object { + "id": 3, + "slug": "other", + }, + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "id": 3, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with table row input', async () => { + const res = await selectQueries.embeded_function_with_table_row_input + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "user_messages": Array [], + "username": "awailas", + }, + Object { + "user_messages": Array [], + "username": "jsonuser", + }, + Object { + "user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with view row input', async () => { + const res = await selectQueries.embeded_function_with_view_row_input + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "active_user_messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "active_user_messages": Array [], + "username": "awailas", + }, + Object { + "active_user_messages": Array [], + "username": "jsonuser", + }, + Object { + "active_user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning view', async () => { + const res = await selectQueries.embeded_function_returning_view + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "recent_messages": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "recent_messages": Array [], + "username": "kiwicopple", + }, + Object { + "recent_messages": Array [], + "username": "awailas", + }, + Object { + "recent_messages": Array [], + "username": "jsonuser", + }, + Object { + "recent_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with view input returning view', async () => { + const res = await selectQueries.embeded_function_with_view_input_returning_view + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "recent_messages": Array [ + Object { + "channel_id": 3, + "data": null, + "id": 4, + "message": "Some message on channel wihtout details", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + ], + "username": "supabot", + }, + Object { + "recent_messages": Array [], + "username": "awailas", + }, + Object { + "recent_messages": Array [], + "username": "jsonuser", + }, + Object { + "recent_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function with blurb_message', async () => { + const res = await selectQueries.embeded_function_with_blurb_message + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "user_messages": Array [ + Object { + "blurb_message": "Hel", + "id": 1, + "message": "Hello World 👋", + }, + Object { + "blurb_message": "Per", + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + Object { + "blurb_message": "Som", + "id": 4, + "message": "Some message on channel wihtout details", + }, + ], + "username": "supabot", + }, + Object { + "user_messages": Array [], + "username": "kiwicopple", + }, + Object { + "user_messages": Array [], + "username": "awailas", + }, + Object { + "user_messages": Array [], + "username": "jsonuser", + }, + Object { + "user_messages": Array [], + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + + test('function returning row', async () => { + const res = await selectQueries.embeded_function_returning_row + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST200", + "details": "Searched for a foreign key relationship between 'channels' and 'function_returning_row' in the schema 'public', but no matches were found.", + "hint": null, + "message": "Could not find a relationship between 'channels' and 'function_returning_row' in the schema cache", + }, + "status": 400, + "statusText": "Bad Request", + } + `) + }) + + test('function returning set of rows', async () => { + const res = await selectQueries.embeded_function_returning_set_of_rows + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST200", + "details": "Searched for a foreign key relationship between 'channels' and 'function_returning_set_of_rows' in the schema 'public', but no matches were found.", + "hint": null, + "message": "Could not find a relationship between 'channels' and 'function_returning_set_of_rows' in the schema cache", + }, + "status": 400, + "statusText": "Bad Request", + } + `) + }) +}) diff --git a/test/rpc.ts b/test/get_username_and_status_rpc.ts similarity index 100% rename from test/rpc.ts rename to test/get_username_and_status_rpc.ts diff --git a/test/index.test.ts b/test/index.test.ts index a7f2ffc6..d7d8c837 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -3,4 +3,6 @@ import './relationships' import './filters' import './resource-embedding' import './transforms' -import './rpc' +import './get_username_and_status_rpc' +import './embeded_functions_join' +import './advanced_rpc' diff --git a/test/override-types.test-d.ts b/test/override-types.test-d.ts index dd73c13e..f682326d 100644 --- a/test/override-types.test-d.ts +++ b/test/override-types.test-d.ts @@ -421,6 +421,7 @@ const postgrest = new PostgrestClient(REST_URL) data: string id: number message: string | null + blurb_message: string | null username: string created_at: Date }[] @@ -476,6 +477,7 @@ const postgrest = new PostgrestClient(REST_URL) data: Json id: number message: string | null + blurb_message: string | null username: string }[] test: { created_at: Date; data: string }[] @@ -518,6 +520,7 @@ const postgrest = new PostgrestClient(REST_URL) data: Json channel_id: number message: string | null + blurb_message: string | null }[] }[] > diff --git a/test/select-query-parser/rpc.test-d.ts b/test/select-query-parser/get_username_and_status_rpc.test-d.ts similarity index 96% rename from test/select-query-parser/rpc.test-d.ts rename to test/select-query-parser/get_username_and_status_rpc.test-d.ts index 3f94c11f..b90b2cd4 100644 --- a/test/select-query-parser/rpc.test-d.ts +++ b/test/select-query-parser/get_username_and_status_rpc.test-d.ts @@ -1,4 +1,4 @@ -import { postgrest, selectParams, RPC_NAME } from '../rpc' +import { postgrest, selectParams, RPC_NAME } from '../get_username_and_status_rpc' import { Database } from '../types.override' import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' diff --git a/test/select-query-parser/types.test-d.ts b/test/select-query-parser/types.test-d.ts index d52432df..32dbead6 100644 --- a/test/select-query-parser/types.test-d.ts +++ b/test/select-query-parser/types.test-d.ts @@ -1,6 +1,9 @@ import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' -import { DeduplicateRelationships } from '../../src/select-query-parser/utils' +import { + DeduplicateRelationships, + FindMatchingFunctionByArgs, +} from '../../src/select-query-parser/utils' // Deduplicate exact sames relationships { type rels = [ @@ -53,3 +56,242 @@ import { DeduplicateRelationships } from '../../src/select-query-parser/utils' type result = DeduplicateRelationships expectType>(true) } + +// Tests we find the right function definition when the function is an union (override declarations) +{ + type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] + + type Database = { + public: { + Tables: { + users: { + Row: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + username: string + } + } + } + } + } + type FnUnion = + | { + Args: Record + Returns: undefined + } + | { + Args: { a: string } + Returns: number + } + | { + Args: { b: number } + Returns: string + } + | { + Args: { cid: number; search?: string } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: '*' + to: 'messages' + isOneToOne: false + } + } + | { + Args: { profile_id: number } + Returns: { + id: number + username: string | null + }[] + SetofOptions: { + from: '*' + to: 'user_profiles' + isOneToOne: false + } + } + | { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: 'users' + to: 'messages' + isOneToOne: false + } + } + { + // Test 1: No arguments matching + type NoArgsMatch = FindMatchingFunctionByArgs + type r = TypeEqual< + NoArgsMatch, + { + Args: Record + Returns: undefined + } + > + expectType(true) + } + + { + // Test 2: Single string argument matching + type StringArgMatch = FindMatchingFunctionByArgs + type r = TypeEqual< + StringArgMatch, + { + Args: { a: string } + Returns: number + } + > + expectType(true) + } + + { + // Test 3: Single number argument matching + type NumberArgMatch = FindMatchingFunctionByArgs + type r = TypeEqual< + NumberArgMatch, + { + Args: { b: number } + Returns: string + } + > + expectType(true) + } + + { + // Test 5: Matching with SetofFunction and complex argument (user_row) + type ComplexArgMatch = FindMatchingFunctionByArgs< + FnUnion, + { + user_row: { + age_range: null + catchphrase: null + data: {} + username: 'test-username' + } + } + > + type r = TypeEqual< + ComplexArgMatch, + { + Args: { + user_row: { + age_range: unknown | null + catchphrase: unknown | null + data: Json + username: string + } + } + Returns: { + channel_id: number + data: Json + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: 'users' + to: 'messages' + isOneToOne: false + } + } + > + expectType(true) + } + + { + // Test 6: Invalid arguments should result in never + type InvalidMatch = FindMatchingFunctionByArgs + type r = TypeEqual + expectType(true) + } + + { + // Test 7: Partial arguments should work if no missing required + type PartialMatch = FindMatchingFunctionByArgs + expectType< + TypeEqual< + PartialMatch, + { + Args: { + cid: number + search?: string + } + Returns: { + channel_id: number + data: Json + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: '*' + to: 'messages' + isOneToOne: false + } + } + > + >(true) + type PartialMatchValued = FindMatchingFunctionByArgs + expectType< + TypeEqual< + PartialMatchValued, + { + Args: { + cid: number + search?: string + } + Returns: { + channel_id: number + data: Json + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: '*' + to: 'messages' + isOneToOne: false + } + } + > + >(true) + type PartialMatchMissingRequired = FindMatchingFunctionByArgs + expectType>(true) + } + + { + // Test 8: Extra arguments should result in never + type ExtraArgsMatch = FindMatchingFunctionByArgs + type r = TypeEqual + expectType(true) + } +} + +// Test we are able to use the proper type when the function is a single declaration +{ + type FnSingle = { + Args: Record + Returns: undefined + } + type SingleMatch = FindMatchingFunctionByArgs> + type r = TypeEqual< + SingleMatch, + { + Args: Record + Returns: undefined + } + > + expectType(true) +} diff --git a/test/types.generated.ts b/test/types.generated.ts index 8f228052..ea164915 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -30,9 +30,7 @@ export type Database = { } Functions: { get_status: { - Args: { - name_param: string - } + Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } } @@ -65,6 +63,13 @@ export type Database = { third_wheel?: string | null } Relationships: [ + { + foreignKeyName: 'best_friends_first_user_fkey' + columns: ['first_user'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_first_user_fkey' columns: ['first_user'] @@ -86,6 +91,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_second_user_fkey' + columns: ['second_user'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_second_user_fkey' columns: ['second_user'] @@ -107,6 +119,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_third_wheel_fkey' + columns: ['third_wheel'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_third_wheel_fkey' columns: ['third_wheel'] @@ -278,6 +297,7 @@ export type Database = { id: number message: string | null username: string + blurb_message: string | null } Insert: { channel_id: number @@ -301,6 +321,13 @@ export type Database = { referencedRelation: 'channels' referencedColumns: ['id'] }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'messages_username_fkey' columns: ['username'] @@ -407,6 +434,13 @@ export type Database = { username?: string | null } Relationships: [ + { + foreignKeyName: 'user_profiles_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, { foreignKeyName: 'user_profiles_username_fkey' columns: ['username'] @@ -456,12 +490,82 @@ export type Database = { } } Views: { + active_users: { + Row: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string | null + } + Insert: { + age_range?: unknown | null + catchphrase?: unknown | null + data?: Json | null + status?: Database['public']['Enums']['user_status'] | null + username?: string | null + } + Update: { + age_range?: unknown | null + catchphrase?: unknown | null + data?: Json | null + status?: Database['public']['Enums']['user_status'] | null + username?: string | null + } + Relationships: [] + } non_updatable_view: { Row: { username: string | null } Relationships: [] } + recent_messages: { + Row: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + } + Relationships: [ + { + foreignKeyName: 'messages_channel_id_fkey' + columns: ['channel_id'] + isOneToOne: false + referencedRelation: 'channels' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'active_users' + referencedColumns: ['username'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'non_updatable_view' + referencedColumns: ['username'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'updatable_view' + referencedColumns: ['username'] + }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + } + ] + } updatable_view: { Row: { non_updatable_column: number | null @@ -479,39 +583,331 @@ export type Database = { } } Functions: { - function_with_array_param: { - Args: { - param: string[] + function_returning_row: { + Args: Record + Returns: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string } + } + function_returning_set_of_rows: { + Args: Record + Returns: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string + }[] + } + function_with_array_param: { + Args: { param: string[] } Returns: undefined } function_with_optional_param: { - Args: { - param?: string - } + Args: { param?: string } Returns: string } - get_status: { + get_active_user_messages: { Args: { - name_param: string + active_user_row: Database['public']['Views']['active_users']['Row'] + } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: 'active_users' + to: 'messages' + isOneToOne: false + } + } + get_messages: + | { + Args: { + channel_row: Database['public']['Tables']['channels']['Row'] + } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: 'channels' + to: 'messages' + isOneToOne: false + } + } + | { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: 'users' + to: 'messages' + isOneToOne: false + } + } + get_messages_by_username: { + Args: { search_username: string } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: '*' + to: 'messages' + isOneToOne: false } + } + get_recent_messages_by_username: { + Args: { search_username: string } + Returns: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + }[] + SetofOptions: { + from: '*' + to: 'recent_messages' + isOneToOne: false + } + } + get_status: { + Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } - get_username_and_status: { + get_user_first_message: { Args: { - name_param: string + active_user_row: Database['public']['Views']['active_users']['Row'] + } + Returns: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + } + SetofOptions: { + from: 'active_users' + to: 'recent_messages' + isOneToOne: true } + } + get_user_messages: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } Returns: { + channel_id: number + data: Json | null + id: number + message: string | null username: string + }[] + SetofOptions: { + from: 'users' + to: 'messages' + isOneToOne: false + } + } + get_user_profile: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + id: number + username: string | null + } + SetofOptions: { + from: 'users' + to: 'user_profiles' + isOneToOne: true + } + } + get_user_profile_non_nullable: { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + id: number + username: string | null + } + SetofOptions: { + from: 'users' + to: 'user_profiles' + isOneToOne: true + } + } + get_user_recent_messages: + | { + Args: { + active_user_row: Database['public']['Views']['active_users']['Row'] + } + Returns: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + }[] + SetofOptions: { + from: 'active_users' + to: 'recent_messages' + isOneToOne: false + } + } + | { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + }[] + SetofOptions: { + from: 'users' + to: 'recent_messages' + isOneToOne: false + } + } + get_username_and_status: { + Args: { name_param: string } + Returns: { status: Database['public']['Enums']['user_status'] + username: string }[] } offline_user: { - Args: { - name_param: string - } + Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } + polymorphic_function_with_different_return: { + Args: { '': string } + Returns: string + } + polymorphic_function_with_no_params_or_unnamed: + | { + Args: Record + Returns: number + } + | { + Args: { '': string } + Returns: string + } + polymorphic_function_with_unnamed_default: + | { + Args: Record + Returns: { + error: true + } & 'Could not choose the best candidate function between: polymorphic_function_with_unnamed_default( => int4), polymorphic_function_with_unnamed_default(). Try renaming the parameters or the function itself in the database so function overloading can be resolved' + } + | { + Args: { '': string } + Returns: string + } + polymorphic_function_with_unnamed_default_overload: + | { + Args: Record + Returns: { + error: true + } & 'Could not choose the best candidate function between: polymorphic_function_with_unnamed_default_overload( => int4), polymorphic_function_with_unnamed_default_overload(). Try renaming the parameters or the function itself in the database so function overloading can be resolved' + } + | { + Args: { '': string } + Returns: string + } + polymorphic_function_with_unnamed_json: { + Args: { '': Json } + Returns: number + } + polymorphic_function_with_unnamed_jsonb: { + Args: { '': Json } + Returns: number + } + polymorphic_function_with_unnamed_text: { + Args: { '': string } + Returns: number + } + postgrest_resolvable_with_override_function: + | { + Args: Record + Returns: undefined + } + | { + Args: { a: string } + Returns: number + } + | { + Args: { b: number } + Returns: string + } + | { + Args: { cid: number; search?: string } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: '*' + to: 'messages' + isOneToOne: false + } + } + | { + Args: { profile_id: number } + Returns: { + id: number + username: string | null + }[] + SetofOptions: { + from: '*' + to: 'user_profiles' + isOneToOne: false + } + } + | { + Args: { user_row: Database['public']['Tables']['users']['Row'] } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: { + from: 'users' + to: 'messages' + isOneToOne: false + } + } + postgrest_unresolvable_function: + | { + Args: Record + Returns: undefined + } + | { + Args: { a: unknown } + Returns: { + error: true + } & 'Could not choose the best candidate function between: postgrest_unresolvable_function(a => int4), postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved' + } void_func: { Args: Record Returns: undefined diff --git a/test/types.override.ts b/test/types.override.ts index 65fab3ac..cd563566 100644 --- a/test/types.override.ts +++ b/test/types.override.ts @@ -43,6 +43,13 @@ export type Database = MergeDeep< } } } + Functions: { + get_user_profile_non_nullable: { + SetofOptions: { + isNotNullable: true + } + } + } } } >