From 3dc065b37767cd42f4676426021c32016a8c1543 Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 29 Mar 2025 23:07:49 +0100 Subject: [PATCH 01/15] wip: setup test options --- package.json | 2 +- src/types.ts | 5 +++++ test/db/00-schema.sql | 17 +++++++++++++++++ test/db/docker-compose.yml | 1 - test/types.generated.ts | 25 +++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2942deeb..3645efd0 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" }, "dependencies": { "@supabase/node-fetch": "^2.6.14" diff --git a/src/types.ts b/src/types.ts index 51d58a70..a644c569 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,9 +60,14 @@ export type GenericNonUpdatableView = { export type GenericView = GenericUpdatableView | GenericNonUpdatableView +export type GenericSetofOption = { + isOneToOne?: boolean +} + export type GenericFunction = { Args: Record Returns: unknown + SetofOptions?: GenericSetofOption } export type GenericSchema = { diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index ee3f6e2e..88175e6a 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -163,3 +163,20 @@ 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; +$$; + +CREATE OR REPLACE FUNCTION public.get_messages(chan_id bigint) +RETURNS SETOF messages +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.messages WHERE channel_id = chan_id; +$$; + diff --git a/test/db/docker-compose.yml b/test/db/docker-compose.yml index c2f91a1d..ae5e0c81 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 diff --git a/test/types.generated.ts b/test/types.generated.ts index 8f228052..93b97fce 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -491,12 +491,37 @@ export type Database = { } Returns: string } + get_messages: { + Args: { + chan_id: number + } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + SetofOptions: {} + } get_status: { Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } + get_user_profile: { + Args: { + user_row: Database['public']['Tables']['users']['Row'] + } + Returns: { + id: number + username: string | null + }[] + SetofOptions: { + isOneToOne: true + } + } get_username_and_status: { Args: { name_param: string From 284927e2666ee33bab9757abf589f39cdc55bd05 Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 29 Mar 2025 23:38:58 +0100 Subject: [PATCH 02/15] wip: setup basic runtime tests --- test/db/00-schema.sql | 4 +- test/embeded_functions_join.ts | 244 ++++++++++++++++++ ...{rpc.ts => get_username_and_status_rpc.ts} | 0 test/index.test.ts | 3 +- ... => get_username_and_status_rpc.test-d.ts} | 2 +- test/types.generated.ts | 6 +- 6 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 test/embeded_functions_join.ts rename test/{rpc.ts => get_username_and_status_rpc.ts} (100%) rename test/select-query-parser/{rpc.test-d.ts => get_username_and_status_rpc.test-d.ts} (96%) diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 88175e6a..46b83e4d 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -173,10 +173,10 @@ AS $$ SELECT * FROM public.user_profiles WHERE username = user_row.username; $$; -CREATE OR REPLACE FUNCTION public.get_messages(chan_id bigint) +CREATE OR REPLACE FUNCTION public.get_messages(channel_row channels) RETURNS SETOF messages LANGUAGE SQL STABLE AS $$ - SELECT * FROM public.messages WHERE channel_id = chan_id; + SELECT * FROM public.messages WHERE channel_id = channel_row.id; $$; diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts new file mode 100644 index 00000000..163fb4c3 --- /dev/null +++ b/test/embeded_functions_join.ts @@ -0,0 +1,244 @@ +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_row_one_function: { + from: 'users', + select: 'username, user_called_profile:get_user_profile(*)', + }, + embeded_setof_row_one_function_with_fields_selection: { + from: 'users', + select: 'username, user_called_profile:get_user_profile(username)', + }, +} 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_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_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), +} 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 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", + } + `) + }) +}) + +describe('rpc', () => { + test('function returning a single row embeded table', async () => { + //@ts-expect-error will complain about missing the rest of the params + const res = await postgrest.rpc('get_user_profile', { user_row: { username: 'supabot' } }) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "id": 1, + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) + test('function returning a setof embeded table', async () => { + //@ts-expect-error will complain about missing the rest of the params + const res = await postgrest.rpc('get_messages', { channel_row: { id: 1 } }) + 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", + } + `) + }) +}) 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..9ce355f7 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -3,4 +3,5 @@ import './relationships' import './filters' import './resource-embedding' import './transforms' -import './rpc' +import './get_username_and_status_rpc' +import './embeded_functions_join' 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/types.generated.ts b/test/types.generated.ts index 93b97fce..adbd2476 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -493,7 +493,7 @@ export type Database = { } get_messages: { Args: { - chan_id: number + channel_row: Database['public']['Tables']['channels']['Row'] } Returns: { channel_id: number @@ -502,7 +502,6 @@ export type Database = { message: string | null username: string }[] - SetofOptions: {} } get_status: { Args: { @@ -518,9 +517,6 @@ export type Database = { id: number username: string | null }[] - SetofOptions: { - isOneToOne: true - } } get_username_and_status: { Args: { From 33f8ca9a9debb107291bec007a497eabc8edbb85 Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 29 Mar 2025 23:53:02 +0100 Subject: [PATCH 03/15] wip: setup types test --- src/types.ts | 1 + test/embeded_functions_join.test-d.ts | 46 +++++++++++++++++++++++++++ test/types.override.ts | 13 ++++++++ 3 files changed, 60 insertions(+) create mode 100644 test/embeded_functions_join.test-d.ts diff --git a/src/types.ts b/src/types.ts index a644c569..52921894 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,6 +62,7 @@ export type GenericView = GenericUpdatableView | GenericNonUpdatableView export type GenericSetofOption = { isOneToOne?: boolean + referencedRelation: string } export type GenericFunction = { diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts new file mode 100644 index 00000000..6d726982 --- /dev/null +++ b/test/embeded_functions_join.test-d.ts @@ -0,0 +1,46 @@ +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: { + id: number + all_channels_messages: Array + } + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_function_fields_selection + let result: Exclude + let expected: { + id: number + all_channels_messages: Array> + } + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_row_one_function + let result: Exclude + let expected: { + username: string + user_called_profile: Schema['Tables']['user_profiles']['Row'] | null + } + expectType>(true) +} + +{ + const { data } = await selectQueries.embeded_setof_row_one_function_with_fields_selection + let result: Exclude + let expected: { + username: string + user_called_profile: Pick | null + } + expectType>(true) +} diff --git a/test/types.override.ts b/test/types.override.ts index 65fab3ac..7d70e503 100644 --- a/test/types.override.ts +++ b/test/types.override.ts @@ -43,6 +43,19 @@ export type Database = MergeDeep< } } } + Functions: { + get_messages: { + SetofOptions: { + referencedRelation: 'messages' + } + } + get_user_profile: { + SetofOptions: { + referencedRelation: 'user_profiles' + isOneToOne: true + } + } + } } } > From 5a7cd6eb1a0a5e456605262e6e768c8ea71f3c34 Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 30 Mar 2025 01:32:13 +0100 Subject: [PATCH 04/15] wip: pass basics tests --- src/select-query-parser/result.ts | 4 +- src/select-query-parser/utils.ts | 35 +++++ src/types.ts | 5 +- test/basic.ts | 139 ++++++++++---------- test/db/00-schema.sql | 6 + test/embeded_functions_join.test-d.ts | 59 +++++++-- test/embeded_functions_join.ts | 176 +++++++++++++++++++++++++- test/types.generated.ts | 10 +- test/types.override.ts | 6 +- 9 files changed, 343 insertions(+), 97 deletions(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index a32567c2..a7ede709 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,7 @@ type ProcessEmbeddedResourceResult< Schema extends GenericSchema, Resolved extends { referencedTable: Pick - relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } + relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' } direction: string }, Field extends Ast.FieldNode, diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index bcbfef04..1eee6cd4 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,28 @@ 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' } + 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 +518,18 @@ type ResolveJoinTableRelationship< : never }[keyof TablesAndViews] +type ResolveEmbededFunctionJoinTableRelationship< + Schema extends GenericSchema, + CurrentTableOrView extends keyof TablesAndViews & string, + FieldName extends string +> = Schema['Functions'][FieldName] extends GenericFunction + ? Schema['Functions'][FieldName]['SetofOptions'] extends GenericSetofOption + ? CurrentTableOrView extends Schema['Functions'][FieldName]['SetofOptions']['from'] + ? Schema['Functions'][FieldName]['SetofOptions'] + : false + : false + : false + export type FindJoinTableRelationship< Schema extends GenericSchema, CurrentTableOrView extends keyof TablesAndViews & string, diff --git a/src/types.ts b/src/types.ts index 52921894..789c670c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,8 +61,9 @@ export type GenericNonUpdatableView = { export type GenericView = GenericUpdatableView | GenericNonUpdatableView export type GenericSetofOption = { - isOneToOne?: boolean - referencedRelation: string + isOneToOne?: boolean | undefined + to: string + from: string } export type GenericFunction = { diff --git a/test/basic.ts b/test/basic.ts index d2317cab..55c31281 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -31,13 +31,6 @@ test('basic select table', async () => { "status": "ONLINE", "username": "awailas", }, - Object { - "age_range": "[20,30)", - "catchphrase": "'fat' 'rat'", - "data": null, - "status": "ONLINE", - "username": "dragarcia", - }, Object { "age_range": "[20,30)", "catchphrase": "'json' 'test'", @@ -52,6 +45,13 @@ test('basic select table', async () => { "status": "ONLINE", "username": "jsonuser", }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, ], "error": null, "status": 200, @@ -87,13 +87,6 @@ test('basic select returns types override', async () => { "status": "ONLINE", "username": "awailas", }, - Object { - "age_range": "[20,30)", - "catchphrase": "'fat' 'rat'", - "data": null, - "status": "ONLINE", - "username": "dragarcia", - }, Object { "age_range": "[20,30)", "catchphrase": "'json' 'test'", @@ -108,6 +101,13 @@ test('basic select returns types override', async () => { "status": "ONLINE", "username": "jsonuser", }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, ], "error": null, "status": 200, @@ -220,11 +220,11 @@ test('basic select view', async () => { }, Object { "non_updatable_column": 1, - "username": "dragarcia", + "username": "jsonuser", }, Object { "non_updatable_column": 1, - "username": "jsonuser", + "username": "dragarcia", }, ], "error": null, @@ -461,7 +461,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 1, "data": null, - "id": 5, + "id": 29, "message": "foo", "username": "supabot", }, @@ -492,10 +492,10 @@ describe('basic insert, update, delete', () => { "username": "supabot", }, Object { - "channel_id": 3, + "channel_id": 1, "data": null, - "id": 3, - "message": "Some message on channel wihtout details", + "id": 29, + "message": "foo", "username": "supabot", }, Object { @@ -505,13 +505,6 @@ describe('basic insert, update, delete', () => { "message": "Some message on channel wihtout details", "username": "supabot", }, - Object { - "channel_id": 1, - "data": null, - "id": 5, - "message": "foo", - "username": "supabot", - }, ], "error": null, "status": 200, @@ -538,8 +531,8 @@ describe('basic insert, update, delete', () => { }, ], "error": null, - "status": 200, - "statusText": "OK", + "status": 201, + "statusText": "Created", } `) @@ -563,17 +556,17 @@ describe('basic insert, update, delete', () => { "username": "supabot", }, Object { - "channel_id": 3, + "channel_id": 1, "data": null, - "id": 4, - "message": "Some message on channel wihtout details", + "id": 29, + "message": "foo", "username": "supabot", }, Object { - "channel_id": 1, + "channel_id": 3, "data": null, - "id": 5, - "message": "foo", + "id": 4, + "message": "Some message on channel wihtout details", "username": "supabot", }, Object { @@ -606,14 +599,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 1, "data": null, - "id": 6, + "id": 30, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 7, + "id": 31, "message": "foo", "username": "supabot", }, @@ -644,17 +637,17 @@ describe('basic insert, update, delete', () => { "username": "supabot", }, Object { - "channel_id": 3, + "channel_id": 1, "data": null, - "id": 4, - "message": "Some message on channel wihtout details", + "id": 29, + "message": "foo", "username": "supabot", }, Object { - "channel_id": 1, + "channel_id": 3, "data": null, - "id": 5, - "message": "foo", + "id": 4, + "message": "Some message on channel wihtout details", "username": "supabot", }, Object { @@ -667,14 +660,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 1, "data": null, - "id": 6, + "id": 30, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 7, + "id": 31, "message": "foo", "username": "supabot", }, @@ -721,7 +714,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 5, + "id": 29, "message": "foo", "username": "supabot", }, @@ -735,14 +728,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 6, + "id": 30, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 7, + "id": 31, "message": "foo", "username": "supabot", }, @@ -782,7 +775,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 5, + "id": 29, "message": "foo", "username": "supabot", }, @@ -796,14 +789,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 6, + "id": 30, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 7, + "id": 31, "message": "foo", "username": "supabot", }, @@ -824,7 +817,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 5, + "id": 29, "message": "foo", "username": "supabot", }, @@ -838,14 +831,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 6, + "id": 30, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 7, + "id": 31, "message": "foo", "username": "supabot", }, @@ -1287,7 +1280,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 8, + "id": 32, "message": "foo", "username": "supabot", }, @@ -1327,7 +1320,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 8, + "id": 32, "message": "foo", "username": "supabot", }, @@ -1391,7 +1384,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 8, + "id": 32, "message": "foo", "username": "supabot", }, @@ -1428,14 +1421,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 9, + "id": 33, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 10, + "id": 34, "message": "foo", "username": "supabot", }, @@ -1475,7 +1468,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 8, + "id": 32, "message": "foo", "username": "supabot", }, @@ -1489,14 +1482,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 9, + "id": 33, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 10, + "id": 34, "message": "foo", "username": "supabot", }, @@ -1525,7 +1518,7 @@ describe("insert, update, delete with count: 'exact'", () => { }, Object { "data": null, - "id": 5, + "id": 21, "slug": "test-slug", }, ], @@ -1553,7 +1546,7 @@ describe("insert, update, delete with count: 'exact'", () => { }, Object { "data": null, - "id": 7, + "id": 23, "slug": "test-slug", }, ], @@ -1577,7 +1570,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 8, + "id": 32, "message": "foo", "username": "supabot", }, @@ -1591,14 +1584,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 9, + "id": 33, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 10, + "id": 34, "message": "foo", "username": "supabot", }, @@ -1638,7 +1631,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 8, + "id": 32, "message": "foo", "username": "supabot", }, @@ -1652,14 +1645,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 9, + "id": 33, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 10, + "id": 34, "message": "foo", "username": "supabot", }, @@ -1684,7 +1677,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 8, + "id": 32, "message": "foo", "username": "supabot", }, @@ -1698,14 +1691,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 9, + "id": 33, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 10, + "id": 34, "message": "foo", "username": "supabot", }, diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 46b83e4d..702e7391 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -180,3 +180,9 @@ 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; +$$; diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index 6d726982..95ea0e23 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -1,46 +1,87 @@ import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' import { Database } from './types.override' -import { selectQueries } from './embeded_functions_join' +import { rpcQueries, selectQueries } from './embeded_functions_join' type Schema = Database['public'] { const { data } = await selectQueries.embeded_setof_function let result: Exclude - let expected: { + 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: { + 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: { + 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_with_fields_selection let result: Exclude - let expected: { + let expected: Array<{ username: string user_called_profile: Pick | null - } + }> + expectType>(true) +} + +{ + 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: Array expectType>(true) } diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index 163fb4c3..c9d48a92 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -10,6 +10,14 @@ export const selectParams = { 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(*)', @@ -27,6 +35,12 @@ export const selectQueries = { 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), @@ -127,6 +141,109 @@ describe('select', () => { `) }) + 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(` @@ -201,16 +318,33 @@ describe('select', () => { }) }) -describe('rpc', () => { - test('function returning a single row embeded table', async () => { +export const rpcQueries = { + 'function returning a setof embeded table': postgrest.rpc('get_messages', { + //@ts-expect-error will complain about missing the rest of the params + channel_row: { id: 1 }, + }), + 'function double definition returning a setof embeded table': postgrest.rpc('get_messages', { + //@ts-expect-error will complain about missing the rest of the params + user_row: { username: 'supabot' }, + }), + 'function returning a single row embeded table': postgrest.rpc('get_user_profile', { //@ts-expect-error will complain about missing the rest of the params - const res = await postgrest.rpc('get_user_profile', { user_row: { username: 'supabot' } }) + user_row: { username: 'supabot' }, + }), +} + +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", }, ], @@ -220,9 +354,8 @@ describe('rpc', () => { } `) }) - test('function returning a setof embeded table', async () => { - //@ts-expect-error will complain about missing the rest of the params - const res = await postgrest.rpc('get_messages', { channel_row: { id: 1 } }) + 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, @@ -234,6 +367,37 @@ describe('rpc', () => { "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, diff --git a/test/types.generated.ts b/test/types.generated.ts index adbd2476..adf0ca30 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -492,9 +492,13 @@ export type Database = { Returns: string } get_messages: { - Args: { - channel_row: Database['public']['Tables']['channels']['Row'] - } + Args: + | { + channel_row: Database['public']['Tables']['channels']['Row'] + } + | { + user_row: Database['public']['Tables']['users']['Row'] + } Returns: { channel_id: number data: Json | null diff --git a/test/types.override.ts b/test/types.override.ts index 7d70e503..42bbf8cb 100644 --- a/test/types.override.ts +++ b/test/types.override.ts @@ -46,12 +46,14 @@ export type Database = MergeDeep< Functions: { get_messages: { SetofOptions: { - referencedRelation: 'messages' + to: 'messages' + from: 'channels' | 'users' } } get_user_profile: { SetofOptions: { - referencedRelation: 'user_profiles' + to: 'user_profiles' + from: 'users' isOneToOne: true } } From c9eee0a09000b583b080972270edaeaa7e984236 Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 30 Mar 2025 14:42:21 +0200 Subject: [PATCH 05/15] chore: add more tests --- test/basic.ts | 139 ++++++++++++++------------ test/embeded_functions_join.test-d.ts | 17 ++++ test/embeded_functions_join.ts | 60 +++++++++++ 3 files changed, 150 insertions(+), 66 deletions(-) diff --git a/test/basic.ts b/test/basic.ts index 55c31281..d2317cab 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -31,6 +31,13 @@ test('basic select table', async () => { "status": "ONLINE", "username": "awailas", }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, Object { "age_range": "[20,30)", "catchphrase": "'json' 'test'", @@ -45,13 +52,6 @@ test('basic select table', async () => { "status": "ONLINE", "username": "jsonuser", }, - Object { - "age_range": "[20,30)", - "catchphrase": "'fat' 'rat'", - "data": null, - "status": "ONLINE", - "username": "dragarcia", - }, ], "error": null, "status": 200, @@ -87,6 +87,13 @@ test('basic select returns types override', async () => { "status": "ONLINE", "username": "awailas", }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, Object { "age_range": "[20,30)", "catchphrase": "'json' 'test'", @@ -101,13 +108,6 @@ test('basic select returns types override', async () => { "status": "ONLINE", "username": "jsonuser", }, - Object { - "age_range": "[20,30)", - "catchphrase": "'fat' 'rat'", - "data": null, - "status": "ONLINE", - "username": "dragarcia", - }, ], "error": null, "status": 200, @@ -220,11 +220,11 @@ test('basic select view', async () => { }, Object { "non_updatable_column": 1, - "username": "jsonuser", + "username": "dragarcia", }, Object { "non_updatable_column": 1, - "username": "dragarcia", + "username": "jsonuser", }, ], "error": null, @@ -461,7 +461,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 1, "data": null, - "id": 29, + "id": 5, "message": "foo", "username": "supabot", }, @@ -492,10 +492,10 @@ describe('basic insert, update, delete', () => { "username": "supabot", }, Object { - "channel_id": 1, + "channel_id": 3, "data": null, - "id": 29, - "message": "foo", + "id": 3, + "message": "Some message on channel wihtout details", "username": "supabot", }, Object { @@ -505,6 +505,13 @@ describe('basic insert, update, delete', () => { "message": "Some message on channel wihtout details", "username": "supabot", }, + Object { + "channel_id": 1, + "data": null, + "id": 5, + "message": "foo", + "username": "supabot", + }, ], "error": null, "status": 200, @@ -531,8 +538,8 @@ describe('basic insert, update, delete', () => { }, ], "error": null, - "status": 201, - "statusText": "Created", + "status": 200, + "statusText": "OK", } `) @@ -556,17 +563,17 @@ describe('basic insert, update, delete', () => { "username": "supabot", }, Object { - "channel_id": 1, + "channel_id": 3, "data": null, - "id": 29, - "message": "foo", + "id": 4, + "message": "Some message on channel wihtout details", "username": "supabot", }, Object { - "channel_id": 3, + "channel_id": 1, "data": null, - "id": 4, - "message": "Some message on channel wihtout details", + "id": 5, + "message": "foo", "username": "supabot", }, Object { @@ -599,14 +606,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 1, "data": null, - "id": 30, + "id": 6, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 31, + "id": 7, "message": "foo", "username": "supabot", }, @@ -637,17 +644,17 @@ describe('basic insert, update, delete', () => { "username": "supabot", }, Object { - "channel_id": 1, + "channel_id": 3, "data": null, - "id": 29, - "message": "foo", + "id": 4, + "message": "Some message on channel wihtout details", "username": "supabot", }, Object { - "channel_id": 3, + "channel_id": 1, "data": null, - "id": 4, - "message": "Some message on channel wihtout details", + "id": 5, + "message": "foo", "username": "supabot", }, Object { @@ -660,14 +667,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 1, "data": null, - "id": 30, + "id": 6, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 31, + "id": 7, "message": "foo", "username": "supabot", }, @@ -714,7 +721,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 29, + "id": 5, "message": "foo", "username": "supabot", }, @@ -728,14 +735,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 30, + "id": 6, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 31, + "id": 7, "message": "foo", "username": "supabot", }, @@ -775,7 +782,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 29, + "id": 5, "message": "foo", "username": "supabot", }, @@ -789,14 +796,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 30, + "id": 6, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 31, + "id": 7, "message": "foo", "username": "supabot", }, @@ -817,7 +824,7 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 29, + "id": 5, "message": "foo", "username": "supabot", }, @@ -831,14 +838,14 @@ describe('basic insert, update, delete', () => { Object { "channel_id": 2, "data": null, - "id": 30, + "id": 6, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 31, + "id": 7, "message": "foo", "username": "supabot", }, @@ -1280,7 +1287,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 32, + "id": 8, "message": "foo", "username": "supabot", }, @@ -1320,7 +1327,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 32, + "id": 8, "message": "foo", "username": "supabot", }, @@ -1384,7 +1391,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 32, + "id": 8, "message": "foo", "username": "supabot", }, @@ -1421,14 +1428,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 33, + "id": 9, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 34, + "id": 10, "message": "foo", "username": "supabot", }, @@ -1468,7 +1475,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 32, + "id": 8, "message": "foo", "username": "supabot", }, @@ -1482,14 +1489,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 1, "data": null, - "id": 33, + "id": 9, "message": "foo", "username": "supabot", }, Object { "channel_id": 1, "data": null, - "id": 34, + "id": 10, "message": "foo", "username": "supabot", }, @@ -1518,7 +1525,7 @@ describe("insert, update, delete with count: 'exact'", () => { }, Object { "data": null, - "id": 21, + "id": 5, "slug": "test-slug", }, ], @@ -1546,7 +1553,7 @@ describe("insert, update, delete with count: 'exact'", () => { }, Object { "data": null, - "id": 23, + "id": 7, "slug": "test-slug", }, ], @@ -1570,7 +1577,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 32, + "id": 8, "message": "foo", "username": "supabot", }, @@ -1584,14 +1591,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 33, + "id": 9, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 34, + "id": 10, "message": "foo", "username": "supabot", }, @@ -1631,7 +1638,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 32, + "id": 8, "message": "foo", "username": "supabot", }, @@ -1645,14 +1652,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 33, + "id": 9, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 34, + "id": 10, "message": "foo", "username": "supabot", }, @@ -1677,7 +1684,7 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 32, + "id": 8, "message": "foo", "username": "supabot", }, @@ -1691,14 +1698,14 @@ describe("insert, update, delete with count: 'exact'", () => { Object { "channel_id": 2, "data": null, - "id": 33, + "id": 9, "message": "foo", "username": "supabot", }, Object { "channel_id": 2, "data": null, - "id": 34, + "id": 10, "message": "foo", "username": "supabot", }, diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index 95ea0e23..3e9dda32 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -65,6 +65,23 @@ type Schema = Database['public'] 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 rpcQueries['function returning a setof embeded table'] let result: Exclude diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index c9d48a92..1852816a 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -26,6 +26,10 @@ export const selectParams = { 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))', + }, } as const export const selectQueries = { @@ -47,6 +51,9 @@ export const selectQueries = { 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), } as const describe('select', () => { @@ -316,6 +323,59 @@ describe('select', () => { } `) }) + + 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", + } + `) + }) }) export const rpcQueries = { From 4dfbb0cccabeba8ff3e57e1883b56e9a70d3d2eb Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 30 Mar 2025 17:41:24 +0200 Subject: [PATCH 06/15] feat(types): add isNotNullable optional override to setof function --- src/select-query-parser/result.ts | 11 ++++- src/select-query-parser/utils.ts | 7 +++- src/types.ts | 1 + test/db/00-schema.sql | 10 +++++ test/embeded_functions_join.test-d.ts | 11 ++++- test/embeded_functions_join.ts | 7 ++++ test/types.generated.ts | 58 ++++++++++++++------------- test/types.override.ts | 12 +----- 8 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index a7ede709..de1bb677 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -328,7 +328,10 @@ type ProcessEmbeddedResourceResult< Schema extends GenericSchema, Resolved extends { referencedTable: Pick - relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' } + 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 1eee6cd4..8a3fdb0e 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -469,7 +469,12 @@ export type ResolveForwardRelationship< : false referencedColumns: [] referencedRelation: FoundEmbededFunctionJoinTableRelation['to'] - } & { match: 'func' } + } & { + match: 'func' + isNotNullable: FoundEmbededFunctionJoinTableRelation['isNotNullable'] extends true + ? true + : false + } direction: 'forward' from: CurrentTableOrView type: 'found-by-embeded-function' diff --git a/src/types.ts b/src/types.ts index 789c670c..5b09427f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,6 +62,7 @@ export type GenericView = GenericUpdatableView | GenericNonUpdatableView export type GenericSetofOption = { isOneToOne?: boolean | undefined + isNotNullable?: boolean | undefined to: string from: string } diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 702e7391..6ddb57b7 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -173,6 +173,16 @@ 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 diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index 3e9dda32..119e5b65 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -54,6 +54,15 @@ type Schema = Database['public'] }> 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 @@ -99,6 +108,6 @@ type Schema = Database['public'] { const { data } = await rpcQueries['function returning a single row embeded table'] let result: Exclude - let expected: Array + let expected: Schema['Tables']['user_profiles']['Row'] expectType>(true) } diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index 1852816a..f82178de 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -22,6 +22,10 @@ export const selectParams = { 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)', @@ -48,6 +52,9 @@ export const selectQueries = { 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), diff --git a/test/types.generated.ts b/test/types.generated.ts index adf0ca30..168d1cd5 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'] } } @@ -480,25 +478,17 @@ export type Database = { } Functions: { function_with_array_param: { - Args: { - param: string[] - } + Args: { param: string[] } Returns: undefined } function_with_optional_param: { - Args: { - param?: string - } + Args: { param?: string } Returns: string } get_messages: { Args: - | { - channel_row: Database['public']['Tables']['channels']['Row'] - } - | { - user_row: Database['public']['Tables']['users']['Row'] - } + | { channel_row: Database['public']['Tables']['channels']['Row'] } + | { user_row: Database['public']['Tables']['users']['Row'] } Returns: { channel_id: number data: Json | null @@ -506,35 +496,49 @@ export type Database = { message: string | null username: string }[] + SetofOptions: { + from: 'channels' | 'users' + to: 'messages' + isOneToOne: false + } } get_status: { - Args: { - name_param: string - } + Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } get_user_profile: { - Args: { - user_row: Database['public']['Tables']['users']['Row'] + 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_username_and_status: { - Args: { - name_param: string - } + Args: { name_param: string } Returns: { username: string status: Database['public']['Enums']['user_status'] }[] } offline_user: { - Args: { - name_param: string - } + Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } void_func: { diff --git a/test/types.override.ts b/test/types.override.ts index 42bbf8cb..cd563566 100644 --- a/test/types.override.ts +++ b/test/types.override.ts @@ -44,17 +44,9 @@ export type Database = MergeDeep< } } Functions: { - get_messages: { + get_user_profile_non_nullable: { SetofOptions: { - to: 'messages' - from: 'channels' | 'users' - } - } - get_user_profile: { - SetofOptions: { - to: 'user_profiles' - from: 'users' - isOneToOne: true + isNotNullable: true } } } From de693cad39b700b3dea10da13fc408c7599e59c2 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 1 Apr 2025 19:02:21 +0200 Subject: [PATCH 07/15] chore: add tests for embeded views --- test/db/00-schema.sql | 55 ++++ test/embeded_functions_join.test-d.ts | 75 +++++ test/embeded_functions_join.ts | 439 ++++++++++++++++++++++++++ test/types.generated.ts | 176 +++++++++++ 4 files changed, 745 insertions(+) diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 6ddb57b7..75445562 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -196,3 +196,58 @@ 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; +$$; \ No newline at end of file diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index 119e5b65..7677068b 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -111,3 +111,78 @@ type Schema = Database['public'] let expected: Schema['Tables']['user_profiles']['Row'] 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 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) +} diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index f82178de..558e11a9 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -34,6 +34,22 @@ export const selectParams = { 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(*)', + }, } as const export const selectQueries = { @@ -61,6 +77,18 @@ export const selectQueries = { 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), } as const describe('select', () => { @@ -383,6 +411,222 @@ describe('select', () => { } `) }) + + 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", + } + `) + }) }) export const rpcQueries = { @@ -398,6 +642,24 @@ export const rpcQueries = { //@ts-expect-error will complain about missing the rest of the params user_row: { username: 'supabot' }, }), + 'function with scalar input': postgrest.rpc('get_messages_by_username', { + search_username: 'supabot', + }), + 'function with table row input': postgrest.rpc('get_user_messages', { + //@ts-expect-error will complain about missing the rest of the params + user_row: { username: 'supabot' }, + }), + 'function with view row input': postgrest.rpc('get_active_user_messages', { + //@ts-expect-error will complain about missing the rest of the params + active_user_row: { username: 'supabot', status: 'ONLINE' }, + }), + 'function returning view': postgrest.rpc('get_user_recent_messages', { + //@ts-expect-error will complain about missing the rest of the params + user_row: { username: 'supabot' }, + }), + 'function with scalar input returning view': postgrest.rpc('get_recent_messages_by_username', { + search_username: 'supabot', + }), } describe('rpc', () => { @@ -421,6 +683,7 @@ describe('rpc', () => { } `) }) + 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(` @@ -455,6 +718,7 @@ describe('rpc', () => { } `) }) + test('function returning a single row embeded table', async () => { const res = await rpcQueries['function returning a single row embeded table'] expect(res).toMatchInlineSnapshot(` @@ -472,4 +736,179 @@ describe('rpc', () => { } `) }) + + 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", + } + `) + }) }) diff --git a/test/types.generated.ts b/test/types.generated.ts index 168d1cd5..5e4ef6bb 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -63,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'] @@ -84,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'] @@ -105,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'] @@ -299,6 +320,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'] @@ -405,6 +433,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'] @@ -454,12 +489,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 @@ -485,6 +590,23 @@ export type Database = { Args: { param?: string } Returns: string } + get_active_user_messages: { + Args: { + 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'] } @@ -502,10 +624,45 @@ export type Database = { isOneToOne: false } } + get_messages_by_username: { + Args: { search_username: string } + Returns: { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + } + 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 + }[] + } get_status: { Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } + 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: { @@ -530,6 +687,25 @@ export type Database = { isOneToOne: true } } + get_user_recent_messages: { + Args: + | { user_row: Database['public']['Tables']['users']['Row'] } + | { + 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: 'users' | 'active_users' + to: 'recent_messages' + isOneToOne: false + } + } get_username_and_status: { Args: { name_param: string } Returns: { From e16b6088619e4b8bdf8cf8732b1bbb5b1f465c6d Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 2 Apr 2025 23:41:33 +0200 Subject: [PATCH 08/15] fix(rpc): selection over rpc resulting on setof Related: https://github.com/supabase/supabase-js/issues/1366 --- src/PostgrestClient.ts | 12 +++-- test/embeded_functions_join.test-d.ts | 28 +++++++++++ test/embeded_functions_join.ts | 71 +++++++++++++++++++++++++++ test/types.override.ts | 7 +++ 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index 8a37b09c..33f47f5b 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -2,7 +2,7 @@ import PostgrestQueryBuilder from './PostgrestQueryBuilder' import PostgrestFilterBuilder from './PostgrestFilterBuilder' import PostgrestBuilder from './PostgrestBuilder' import { DEFAULT_HEADERS } from './constants' -import { Fetch, GenericSchema } from './types' +import { Fetch, GenericSchema, GenericSetofOption } from './types' /** * PostgREST client. @@ -139,10 +139,16 @@ export default class PostgrestClient< ? Fn['Returns'][number] extends Record ? Fn['Returns'][number] : never + : Fn['Returns'] extends Record + ? Fn['Returns'] : never, Fn['Returns'], - FnName, - null + Fn['SetofOptions'] extends GenericSetofOption ? Fn['SetofOptions']['to'] : FnName, + Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['to'] extends keyof Schema['Tables'] + ? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships'] + : Schema['Views'][Fn['SetofOptions']['to']]['Relationships'] + : null > { let method: 'HEAD' | 'GET' | 'POST' const url = new URL(`${this.url}/rpc/${fn}`) diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index 7677068b..2a8ea883 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -186,3 +186,31 @@ type Schema = Database['public'] 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 + } + }> + 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) +} diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index 558e11a9..dad911c5 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -660,6 +660,17 @@ export const rpcQueries = { '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', { + //@ts-expect-error will complain about missing the rest of the params + user_row: { username: 'supabot' }, + }) + .select('id, username, users(username, catchphrase)'), } describe('rpc', () => { @@ -911,4 +922,64 @@ describe('rpc', () => { } `) }) + + 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('function with row input with followup select', async () => { + const res = await rpcQueries['function with row input with followup select'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "id": 1, + "username": "supabot", + "users": Object { + "catchphrase": "'cat' 'fat'", + "username": "supabot", + }, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + }) }) diff --git a/test/types.override.ts b/test/types.override.ts index cd563566..3fac7f34 100644 --- a/test/types.override.ts +++ b/test/types.override.ts @@ -49,6 +49,13 @@ export type Database = MergeDeep< isNotNullable: true } } + get_recent_messages_by_username: { + SetofOptions: { + from: '*' + to: 'messages' + isOneToOne: false + } + } } } } From 08c8c75a8a4f006998dfbb51e41a9b7174bb43bf Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 3 Apr 2025 00:13:54 +0200 Subject: [PATCH 09/15] chore: use generated types --- test/embeded_functions_join.test-d.ts | 2 +- test/types.generated.ts | 12 +++++++++++- test/types.override.ts | 7 ------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index 2a8ea883..3fc9a814 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -196,7 +196,7 @@ type Schema = Database['public'] users: { catchphrase: unknown username: string - } + } | null }> expectType>(true) } diff --git a/test/types.generated.ts b/test/types.generated.ts index 5e4ef6bb..591e7138 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -633,6 +633,11 @@ export type Database = { message: string | null username: string }[] + SetofOptions: { + from: '*' + to: 'messages' + isOneToOne: false + } } get_recent_messages_by_username: { Args: { search_username: string } @@ -643,6 +648,11 @@ export type Database = { message: string | null username: string | null }[] + SetofOptions: { + from: '*' + to: 'recent_messages' + isOneToOne: false + } } get_status: { Args: { name_param: string } @@ -701,7 +711,7 @@ export type Database = { username: string | null }[] SetofOptions: { - from: 'users' | 'active_users' + from: 'active_users' | 'users' to: 'recent_messages' isOneToOne: false } diff --git a/test/types.override.ts b/test/types.override.ts index 3fac7f34..cd563566 100644 --- a/test/types.override.ts +++ b/test/types.override.ts @@ -49,13 +49,6 @@ export type Database = MergeDeep< isNotNullable: true } } - get_recent_messages_by_username: { - SetofOptions: { - from: '*' - to: 'messages' - isOneToOne: false - } - } } } } From d14e642888846aba2026e3af0e618ddbdd564c23 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 3 Apr 2025 00:37:36 +0200 Subject: [PATCH 10/15] fix: typeless rpc call types --- src/PostgrestClient.ts | 51 ++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index 33f47f5b..8c3accf1 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -3,6 +3,7 @@ import PostgrestFilterBuilder from './PostgrestFilterBuilder' import PostgrestBuilder from './PostgrestBuilder' import { DEFAULT_HEADERS } from './constants' import { Fetch, GenericSchema, GenericSetofOption } from './types' +import { IsAny } from './select-query-parser/utils' /** * PostgREST client. @@ -133,23 +134,39 @@ export default class PostgrestClient< get?: boolean count?: 'exact' | 'planned' | 'estimated' } = {} - ): PostgrestFilterBuilder< - Schema, - Fn['Returns'] extends any[] - ? Fn['Returns'][number] extends Record - ? Fn['Returns'][number] - : never - : Fn['Returns'] extends Record - ? Fn['Returns'] - : never, - Fn['Returns'], - Fn['SetofOptions'] extends GenericSetofOption ? Fn['SetofOptions']['to'] : FnName, - 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 rpc is called with a typeless client, default to infering everything as any + ): IsAny extends true + ? PostgrestFilterBuilder< + Schema, + Fn['Returns'] extends any[] + ? Fn['Returns'][number] extends Record + ? Fn['Returns'][number] + : never + : Fn['Returns'] extends Record + ? Fn['Returns'] + : never, + Fn['Returns'], + FnName, + null + > + : PostgrestFilterBuilder< + // otherwise, provide the right params for typed .select chaining + Schema, + Fn['Returns'] extends any[] + ? Fn['Returns'][number] extends Record + ? Fn['Returns'][number] + : never + : Fn['Returns'] extends Record + ? Fn['Returns'] + : never, + Fn['Returns'], + Fn['SetofOptions'] extends GenericSetofOption ? Fn['SetofOptions']['to'] : FnName, + Fn['SetofOptions'] extends GenericSetofOption + ? Fn['SetofOptions']['to'] extends keyof Schema['Tables'] + ? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships'] + : Schema['Views'][Fn['SetofOptions']['to']]['Relationships'] + : null + > { let method: 'HEAD' | 'GET' | 'POST' const url = new URL(`${this.url}/rpc/${fn}`) let body: unknown | undefined From eee46120f83651abb21f63f60fcd29504d46e648 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 3 Apr 2025 00:57:22 +0200 Subject: [PATCH 11/15] fix(rpc): select chained with filter Related: https://github.com/supabase/supabase-js/issues/1365 --- src/PostgrestTransformBuilder.ts | 5 ++-- test/embeded_functions_join.ts | 40 ++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 14 deletions(-) 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/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index dad911c5..22d25cc3 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -961,21 +961,37 @@ describe('rpc', () => { `) }) - test('function with row input with followup select', async () => { - const res = await rpcQueries['function with row input with followup select'] + 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 [ - Object { - "id": 1, - "username": "supabot", - "users": Object { - "catchphrase": "'cat' 'fat'", - "username": "supabot", - }, - }, - ], + "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", From 2a40d4f081b09c05eb41881b192340165f80d341 Mon Sep 17 00:00:00 2001 From: avallete Date: Sun, 20 Apr 2025 21:50:27 +0200 Subject: [PATCH 12/15] wip: use function picker --- src/PostgrestClient.ts | 130 +++-- src/select-query-parser/utils.ts | 55 +- test/db/00-schema.sql | 45 +- test/db/docker-compose.yml | 2 +- test/embeded_functions_join.test-d.ts | 112 ++++ test/embeded_functions_join.ts | 375 +++++++++++- test/select-query-parser/types.test-d.ts | 710 ++++++++++++++++++++++- test/types.generated.ts | 209 +++++-- 8 files changed, 1541 insertions(+), 97 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index 8c3accf1..bb74d87a 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -2,8 +2,80 @@ import PostgrestQueryBuilder from './PostgrestQueryBuilder' import PostgrestFilterBuilder from './PostgrestFilterBuilder' import PostgrestBuilder from './PostgrestBuilder' import { DEFAULT_HEADERS } from './constants' -import { Fetch, GenericSchema, GenericSetofOption } from './types' -import { IsAny } from './select-query-parser/utils' +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 + : Args extends GenericFunction['Args'] + ? FindMatchingFunctionByArgs + : any +}[1] extends infer Fn + ? IsAny extends true + ? { Row: any; Result: any; RelationName: FnName; Relationships: null } + : 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 + } + : Fn extends never + ? { + Row: any + Result: { error: true } & "Couldn't find function" + RelationName: FnName + Relationships: null + } + : never + : never + +export type RpcRowType< + Schema extends GenericSchema, + FnName extends string & keyof Schema['Functions'], + Args +> = { + 0: Schema['Functions'][FnName] + 1: Args extends GenericFunction['Args'] + ? FindMatchingFunctionByArgs + : any +}[1] extends infer Fn + ? IsAny extends true + ? any + : Fn extends GenericFunction + ? Fn['Returns'] + : never + : never /** * PostgREST client. @@ -122,9 +194,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, @@ -134,39 +214,13 @@ export default class PostgrestClient< get?: boolean count?: 'exact' | 'planned' | 'estimated' } = {} - // if rpc is called with a typeless client, default to infering everything as any - ): IsAny extends true - ? PostgrestFilterBuilder< - Schema, - Fn['Returns'] extends any[] - ? Fn['Returns'][number] extends Record - ? Fn['Returns'][number] - : never - : Fn['Returns'] extends Record - ? Fn['Returns'] - : never, - Fn['Returns'], - FnName, - null - > - : PostgrestFilterBuilder< - // otherwise, provide the right params for typed .select chaining - Schema, - Fn['Returns'] extends any[] - ? Fn['Returns'][number] extends Record - ? Fn['Returns'][number] - : never - : Fn['Returns'] extends Record - ? Fn['Returns'] - : never, - Fn['Returns'], - Fn['SetofOptions'] extends GenericSetofOption ? Fn['SetofOptions']['to'] : FnName, - Fn['SetofOptions'] extends GenericSetofOption - ? Fn['SetofOptions']['to'] extends keyof Schema['Tables'] - ? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships'] - : Schema['Views'][Fn['SetofOptions']['to']]['Relationships'] - : null - > { + ): PostgrestFilterBuilder< + Schema, + FilterBuilder['Row'], + FilterBuilder['Result'], + FilterBuilder['RelationName'], + FilterBuilder['Relationships'] + > { let method: 'HEAD' | 'GET' | 'POST' const url = new URL(`${this.url}/rpc/${fn}`) let body: unknown | undefined @@ -199,6 +253,6 @@ export default class PostgrestClient< body, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + } as unknown as PostgrestBuilder) } } diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 8a3fdb0e..33e54862 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -527,11 +527,12 @@ type ResolveEmbededFunctionJoinTableRelationship< Schema extends GenericSchema, CurrentTableOrView extends keyof TablesAndViews & string, FieldName extends string -> = Schema['Functions'][FieldName] extends GenericFunction - ? Schema['Functions'][FieldName]['SetofOptions'] extends GenericSetofOption - ? CurrentTableOrView extends Schema['Functions'][FieldName]['SetofOptions']['from'] - ? Schema['Functions'][FieldName]['SetofOptions'] - : false +> = FindMatchingFunctionBySetofFrom< + Schema['Functions'][FieldName], + CurrentTableOrView +> extends infer Fn + ? Fn extends GenericFunction + ? Fn['SetofOptions'] : false : false @@ -618,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 + : never diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 75445562..0acfb82e 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -250,4 +250,47 @@ RETURNS SETOF recent_messages LANGUAGE SQL STABLE AS $$ SELECT * FROM public.recent_messages WHERE username = active_user_row.username; -$$; \ No newline at end of file +$$; +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 'toto' +$$; +-- 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 'toto' +$$; +-- 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(text) returns void language sql as ''; + +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 1'; +create or replace function public.polymorphic_function_with_no_params_or_unnamed(text) returns void language sql as ''; +-- 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'; \ No newline at end of file diff --git a/test/db/docker-compose.yml b/test/db/docker-compose.yml index ae5e0c81..d6ed77d7 100644 --- a/test/db/docker-compose.yml +++ b/test/db/docker-compose.yml @@ -28,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 index 3fc9a814..5f695610 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -2,6 +2,7 @@ import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' import { Database } from './types.override' import { rpcQueries, selectQueries } from './embeded_functions_join' +import { SelectQueryError } from '../src/select-query-parser/utils' type Schema = Database['public'] @@ -214,3 +215,114 @@ type Schema = Database['public'] }> 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: undefined + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with bool param'] + let result: Exclude + let expected: number + expectType>(true) +} + +{ + const { data } = await rpcQueries['polymorphic function with unnamed int param'] + let result: Exclude + // Should be an error response as function is not found + let expected: never + 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) +} diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index 22d25cc3..cfe1692b 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -631,31 +631,55 @@ describe('select', () => { export const rpcQueries = { 'function returning a setof embeded table': postgrest.rpc('get_messages', { - //@ts-expect-error will complain about missing the rest of the params - channel_row: { id: 1 }, + channel_row: { id: 1, data: null, slug: null }, }), 'function double definition returning a setof embeded table': postgrest.rpc('get_messages', { - //@ts-expect-error will complain about missing the rest of the params - user_row: { username: 'supabot' }, + 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', { - //@ts-expect-error will complain about missing the rest of the params - user_row: { username: 'supabot' }, + 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', { - //@ts-expect-error will complain about missing the rest of the params - user_row: { username: 'supabot' }, + user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, }), 'function with view row input': postgrest.rpc('get_active_user_messages', { - //@ts-expect-error will complain about missing the rest of the params - active_user_row: { username: 'supabot', status: 'ONLINE' }, + active_user_row: { + username: 'supabot', + data: null, + age_range: null, + catchphrase: null, + status: 'ONLINE', + }, }), 'function returning view': postgrest.rpc('get_user_recent_messages', { - //@ts-expect-error will complain about missing the rest of the params - user_row: { username: 'supabot' }, + 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', @@ -667,11 +691,100 @@ export const rpcQueries = { .select('channel_id, message, users(username, catchphrase)'), 'function with row input with followup select': postgrest .rpc('get_user_profile', { - //@ts-expect-error will complain about missing the rest of the params - user_row: { username: 'supabot' }, + 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 the unnamed boolean type definition + '': true, + } + ), + 'polymorphic function with unnamed int param': postgrest.rpc( + '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', + } + ), +} as const describe('rpc', () => { test('function returning a setof embeded table', async () => { @@ -998,4 +1111,236 @@ describe('rpc', () => { } `) }) + + 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": "toto", + "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": null, + "error": null, + "status": 204, + "statusText": "No Content", + } + `) + }) + + test('polymorphic function with bool param', async () => { + const res = await rpcQueries['polymorphic function with bool param'] + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 204, + "statusText": "No Content", + } + `) + }) + + 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", + } + `) + }) }) diff --git a/test/select-query-parser/types.test-d.ts b/test/select-query-parser/types.test-d.ts index d52432df..bc848bb3 100644 --- a/test/select-query-parser/types.test-d.ts +++ b/test/select-query-parser/types.test-d.ts @@ -1,6 +1,10 @@ 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' +import { RpcRowType } from '../../src/PostgrestClient' // Deduplicate exact sames relationships { type rels = [ @@ -53,3 +57,707 @@ 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) +} + +{ + 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 + } + Update: {} + Insert: {} + Relationships: [] + } + channels: { + Row: { + data: Json | null + id: number + slug: string | null + } + Insert: { + data?: Json | null + id?: number + slug?: string | null + } + Update: { + data?: Json | null + id?: number + slug?: string | null + } + Relationships: [] + } + } + Functions: { + function_with_array_param: { + Args: { param: string[] } + Returns: undefined + } + function_with_optional_param: { + Args: { param?: string } + Returns: string + } + get_active_user_messages: { + Args: { + 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_user_first_message: { + 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: 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 } + Returns: Database['public']['Enums']['user_status'] + } + polymorphic_function_with_different_return: { + Args: { '': string } + Returns: undefined + } + polymorphic_function_with_no_params_or_unnamed: { + Args: { '': boolean } + Returns: number + } + polymorphic_function_with_unnamed_integer: { + Args: { '': number } + Returns: number + } + 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: { 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' + } + | { + Args: Record + Returns: undefined + } + void_func: { + Args: Record + Returns: undefined + } + } + Enums: { + user_status: 'ONLINE' | 'OFFLINE' + } + CompositeTypes: { + [_ in never]: never + } + 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: [] + } + recent_messages: { + Row: { + channel_id: number | null + data: Json | null + id: number | null + message: string | null + username: string | null + } + Relationships: [] + } + } + } + } + type Schema = Database['public'] + const fnRpc = < + FnName extends string & keyof Schema['Functions'], + Args extends Schema['Functions'][FnName]['Args'] + >( + _fn: FnName, + _args: Args = {} as Args + ): RpcRowType => true as any + + // Test 1: No arguments case + const noArgsRes = fnRpc('postgrest_resolvable_with_override_function', {}) + expectType>(true) + + // Test 2: String argument 'a' + const stringArgRes = fnRpc('postgrest_resolvable_with_override_function', { a: 'test' }) + expectType>(true) + + // Test 3: Number argument 'b' + const numberArgRes = fnRpc('postgrest_resolvable_with_override_function', { b: 42 }) + expectType>(true) + + // Test 4: Channel search with required and optional params + const channelSearchReqOnly = fnRpc('postgrest_resolvable_with_override_function', { cid: 123 }) + const channelSearchWithOpt = fnRpc('postgrest_resolvable_with_override_function', { + cid: 123, + search: 'query', + }) + type ChannelSearchReturn = { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + expectType>(true) + expectType>(true) + + // Test 5: Profile lookup + const profileRes = fnRpc('postgrest_resolvable_with_override_function', { profile_id: 456 }) + type ProfileReturn = { + id: number + username: string | null + }[] + expectType>(true) + + // Test 6: Complex user_row argument + const userRowRes = fnRpc('postgrest_resolvable_with_override_function', { + user_row: { + age_range: null, + catchphrase: null, + data: { some: 'data' }, + username: 'testuser', + }, + }) + type UserRowReturn = { + channel_id: number + data: Json | null + id: number + message: string | null + username: string + }[] + expectType>(true) + + // Test 7: Invalid cases - these should cause type errors + // @ts-expect-error - Invalid argument name + fnRpc('postgrest_resolvable_with_override_function', { invalid: 'arg' }) + + // @ts-expect-error - Missing required argument + fnRpc('postgrest_resolvable_with_override_function', { search: 'query' }) + + // @ts-expect-error - Wrong argument type + fnRpc('postgrest_resolvable_with_override_function', { a: 42 }) + + // Call with extra arguement should result in a never + const extraArgsRes = fnRpc('postgrest_resolvable_with_override_function', { + a: 'test', + extra: true, + }) + expectType>(true) +} + +{ + type Schema = any + const fnRpc = < + FnName extends string & keyof Schema['Functions'], + Args extends Schema['Functions'][FnName]['Args'] + >( + _fn: FnName, + _args: Args = {} as Args + ): RpcRowType => true as any + const extraArgsRes = fnRpc('postgrest_resolvable_with_override_function', { + a: 'test', + extra: true, + }) + expectType>(true) +} diff --git a/test/types.generated.ts b/test/types.generated.ts index 591e7138..34933171 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -1,4 +1,4 @@ -export type Json = unknown +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] export type Database = { personal: { @@ -607,23 +607,39 @@ export type Database = { isOneToOne: false } } - get_messages: { - Args: - | { channel_row: Database['public']['Tables']['channels']['Row'] } - | { user_row: Database['public']['Tables']['users']['Row'] } - Returns: { - channel_id: number - data: Json | null - id: number - message: string | null - username: string - }[] - SetofOptions: { - from: 'channels' | '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: { @@ -658,6 +674,23 @@ export type Database = { Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } + get_user_first_message: { + 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: true + } + } get_user_messages: { Args: { user_row: Database['public']['Tables']['users']['Row'] } Returns: { @@ -697,36 +730,140 @@ export type Database = { isOneToOne: true } } - get_user_recent_messages: { - Args: - | { user_row: Database['public']['Tables']['users']['Row'] } - | { + 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' | 'users' - to: 'recent_messages' - isOneToOne: false - } - } + 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: { - username: string status: Database['public']['Enums']['user_status'] + username: string }[] } offline_user: { Args: { name_param: string } Returns: Database['public']['Enums']['user_status'] } + polymorphic_function_with_different_return: { + Args: { '': string } + Returns: undefined + } + polymorphic_function_with_no_params_or_unnamed: { + Args: { '': boolean } + Returns: number + } + polymorphic_function_with_unnamed_integer: { + Args: { '': number } + Returns: number + } + 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: { 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' + } + | { + Args: Record + Returns: undefined + } void_func: { Args: Record Returns: undefined From 5fec334e8fe181d995534e67fd7bc628eb707ab3 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 21 Apr 2025 14:12:10 +0200 Subject: [PATCH 13/15] feat: add rpc typing with union of functions definitions --- package.json | 2 +- src/PostgrestClient.ts | 30 +- test/advanced_rpc.test-d.ts | 289 +++++++ test/advanced_rpc.ts | 923 +++++++++++++++++++++++ test/db/00-schema.sql | 25 +- test/embeded_functions_join.test-d.ts | 198 +---- test/embeded_functions_join.ts | 716 ------------------ test/index.test.ts | 1 + test/select-query-parser/types.test-d.ts | 466 ------------ test/types.generated.ts | 49 +- 10 files changed, 1279 insertions(+), 1420 deletions(-) create mode 100644 test/advanced_rpc.test-d.ts create mode 100644 test/advanced_rpc.ts diff --git a/package.json b/package.json index 3645efd0..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" + "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 bb74d87a..e27d7c5d 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -25,13 +25,17 @@ export type GetRpcFunctionFilterBuilderByArgs< // args function definition if there is one in such case 1: [keyof Args] extends [never] ? ExtractExactFunction - : Args extends GenericFunction['Args'] + : // Otherwise, we attempt to match with one of the function definition in the union based + // on the function arguments provided + Args extends GenericFunction['Args'] ? FindMatchingFunctionByArgs : any }[1] extends infer Fn - ? IsAny extends true + ? // If we are dealing with an non-typed client everything is any + IsAny extends true ? { Row: any; Result: any; RelationName: FnName; Relationships: null } - : Fn extends GenericFunction + : // 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 @@ -50,7 +54,8 @@ export type GetRpcFunctionFilterBuilderByArgs< : Schema['Views'][Fn['SetofOptions']['to']]['Relationships'] : null } - : Fn extends never + : // If we failed to find the function by argument, we still pass with any but also add an overridable + Fn extends never ? { Row: any Result: { error: true } & "Couldn't find function" @@ -60,23 +65,6 @@ export type GetRpcFunctionFilterBuilderByArgs< : never : never -export type RpcRowType< - Schema extends GenericSchema, - FnName extends string & keyof Schema['Functions'], - Args -> = { - 0: Schema['Functions'][FnName] - 1: Args extends GenericFunction['Args'] - ? FindMatchingFunctionByArgs - : any -}[1] extends infer Fn - ? IsAny extends true - ? any - : Fn extends GenericFunction - ? Fn['Returns'] - : never - : never - /** * PostgREST client. * diff --git a/test/advanced_rpc.test-d.ts b/test/advanced_rpc.test-d.ts new file mode 100644 index 00000000..39f712a4 --- /dev/null +++ b/test/advanced_rpc.test-d.ts @@ -0,0 +1,289 @@ +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) +} diff --git a/test/advanced_rpc.ts b/test/advanced_rpc.ts new file mode 100644 index 00000000..c47debb0 --- /dev/null +++ b/test/advanced_rpc.ts @@ -0,0 +1,923 @@ +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, + } + ), +} 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", + } + `) + }) +}) diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 0acfb82e..93a836fb 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -263,13 +263,13 @@ $$; 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 'toto' + 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 'toto' + 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 $$ @@ -284,13 +284,26 @@ create function postgrest_resolvable_with_override_function(user_row users) retu $$; 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(text) returns void language sql as ''; +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 1'; -create or replace function public.polymorphic_function_with_no_params_or_unnamed(text) returns void language sql as ''; +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'; \ No newline at end of file +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'; + diff --git a/test/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index 5f695610..e5d268c8 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -1,8 +1,7 @@ import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' import { Database } from './types.override' -import { rpcQueries, selectQueries } from './embeded_functions_join' -import { SelectQueryError } from '../src/select-query-parser/utils' +import { selectQueries } from './embeded_functions_join' type Schema = Database['public'] @@ -92,27 +91,6 @@ type Schema = Database['public'] expectType>(true) } -{ - 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 selectQueries.embeded_function_with_table_row_input let result: Exclude @@ -152,177 +130,3 @@ type Schema = Database['public'] }> 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: undefined - expectType>(true) -} - -{ - const { data } = await rpcQueries['polymorphic function with bool param'] - let result: Exclude - let expected: number - expectType>(true) -} - -{ - const { data } = await rpcQueries['polymorphic function with unnamed int param'] - let result: Exclude - // Should be an error response as function is not found - let expected: never - 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) -} diff --git a/test/embeded_functions_join.ts b/test/embeded_functions_join.ts index cfe1692b..b4be5aa0 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -628,719 +628,3 @@ describe('select', () => { `) }) }) - -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 the unnamed boolean type definition - '': true, - } - ), - 'polymorphic function with unnamed int param': postgrest.rpc( - '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', - } - ), -} 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": "toto", - "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": null, - "error": null, - "status": 204, - "statusText": "No Content", - } - `) - }) - - test('polymorphic function with bool param', async () => { - const res = await rpcQueries['polymorphic function with bool param'] - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": null, - "error": null, - "status": 204, - "statusText": "No Content", - } - `) - }) - - 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", - } - `) - }) -}) diff --git a/test/index.test.ts b/test/index.test.ts index 9ce355f7..d7d8c837 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -5,3 +5,4 @@ import './resource-embedding' import './transforms' import './get_username_and_status_rpc' import './embeded_functions_join' +import './advanced_rpc' diff --git a/test/select-query-parser/types.test-d.ts b/test/select-query-parser/types.test-d.ts index bc848bb3..32dbead6 100644 --- a/test/select-query-parser/types.test-d.ts +++ b/test/select-query-parser/types.test-d.ts @@ -4,7 +4,6 @@ import { DeduplicateRelationships, FindMatchingFunctionByArgs, } from '../../src/select-query-parser/utils' -import { RpcRowType } from '../../src/PostgrestClient' // Deduplicate exact sames relationships { type rels = [ @@ -296,468 +295,3 @@ import { RpcRowType } from '../../src/PostgrestClient' > expectType(true) } - -{ - 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 - } - Update: {} - Insert: {} - Relationships: [] - } - channels: { - Row: { - data: Json | null - id: number - slug: string | null - } - Insert: { - data?: Json | null - id?: number - slug?: string | null - } - Update: { - data?: Json | null - id?: number - slug?: string | null - } - Relationships: [] - } - } - Functions: { - function_with_array_param: { - Args: { param: string[] } - Returns: undefined - } - function_with_optional_param: { - Args: { param?: string } - Returns: string - } - get_active_user_messages: { - Args: { - 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_user_first_message: { - 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: 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 } - Returns: Database['public']['Enums']['user_status'] - } - polymorphic_function_with_different_return: { - Args: { '': string } - Returns: undefined - } - polymorphic_function_with_no_params_or_unnamed: { - Args: { '': boolean } - Returns: number - } - polymorphic_function_with_unnamed_integer: { - Args: { '': number } - Returns: number - } - 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: { 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' - } - | { - Args: Record - Returns: undefined - } - void_func: { - Args: Record - Returns: undefined - } - } - Enums: { - user_status: 'ONLINE' | 'OFFLINE' - } - CompositeTypes: { - [_ in never]: never - } - 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: [] - } - recent_messages: { - Row: { - channel_id: number | null - data: Json | null - id: number | null - message: string | null - username: string | null - } - Relationships: [] - } - } - } - } - type Schema = Database['public'] - const fnRpc = < - FnName extends string & keyof Schema['Functions'], - Args extends Schema['Functions'][FnName]['Args'] - >( - _fn: FnName, - _args: Args = {} as Args - ): RpcRowType => true as any - - // Test 1: No arguments case - const noArgsRes = fnRpc('postgrest_resolvable_with_override_function', {}) - expectType>(true) - - // Test 2: String argument 'a' - const stringArgRes = fnRpc('postgrest_resolvable_with_override_function', { a: 'test' }) - expectType>(true) - - // Test 3: Number argument 'b' - const numberArgRes = fnRpc('postgrest_resolvable_with_override_function', { b: 42 }) - expectType>(true) - - // Test 4: Channel search with required and optional params - const channelSearchReqOnly = fnRpc('postgrest_resolvable_with_override_function', { cid: 123 }) - const channelSearchWithOpt = fnRpc('postgrest_resolvable_with_override_function', { - cid: 123, - search: 'query', - }) - type ChannelSearchReturn = { - channel_id: number - data: Json | null - id: number - message: string | null - username: string - }[] - expectType>(true) - expectType>(true) - - // Test 5: Profile lookup - const profileRes = fnRpc('postgrest_resolvable_with_override_function', { profile_id: 456 }) - type ProfileReturn = { - id: number - username: string | null - }[] - expectType>(true) - - // Test 6: Complex user_row argument - const userRowRes = fnRpc('postgrest_resolvable_with_override_function', { - user_row: { - age_range: null, - catchphrase: null, - data: { some: 'data' }, - username: 'testuser', - }, - }) - type UserRowReturn = { - channel_id: number - data: Json | null - id: number - message: string | null - username: string - }[] - expectType>(true) - - // Test 7: Invalid cases - these should cause type errors - // @ts-expect-error - Invalid argument name - fnRpc('postgrest_resolvable_with_override_function', { invalid: 'arg' }) - - // @ts-expect-error - Missing required argument - fnRpc('postgrest_resolvable_with_override_function', { search: 'query' }) - - // @ts-expect-error - Wrong argument type - fnRpc('postgrest_resolvable_with_override_function', { a: 42 }) - - // Call with extra arguement should result in a never - const extraArgsRes = fnRpc('postgrest_resolvable_with_override_function', { - a: 'test', - extra: true, - }) - expectType>(true) -} - -{ - type Schema = any - const fnRpc = < - FnName extends string & keyof Schema['Functions'], - Args extends Schema['Functions'][FnName]['Args'] - >( - _fn: FnName, - _args: Args = {} as Args - ): RpcRowType => true as any - const extraArgsRes = fnRpc('postgrest_resolvable_with_override_function', { - a: 'test', - extra: true, - }) - expectType>(true) -} diff --git a/test/types.generated.ts b/test/types.generated.ts index 34933171..03dde130 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -776,16 +776,39 @@ export type Database = { } polymorphic_function_with_different_return: { Args: { '': string } - Returns: undefined - } - polymorphic_function_with_no_params_or_unnamed: { - Args: { '': boolean } - Returns: number - } - polymorphic_function_with_unnamed_integer: { - Args: { '': number } - Returns: number + 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 @@ -854,16 +877,16 @@ export type Database = { } } 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' } - | { - Args: Record - Returns: undefined - } void_func: { Args: Record Returns: undefined From a1dd3185e36fa37840375480044faa9503f36f7d Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 21 Apr 2025 16:18:21 +0200 Subject: [PATCH 14/15] chore: add tests, fix types test --- src/PostgrestClient.ts | 4 +- src/select-query-parser/utils.ts | 2 +- test/advanced_rpc.test-d.ts | 47 +++++++++-- test/advanced_rpc.ts | 104 +++++++++++++++++++++++++ test/db/00-schema.sql | 21 +++++ test/embeded_functions_join.test-d.ts | 27 +++++++ test/embeded_functions_join.ts | 107 ++++++++++++++++++++++++++ test/override-types.test-d.ts | 3 + test/types.generated.ts | 23 +++++- 9 files changed, 327 insertions(+), 11 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index e27d7c5d..cfde1697 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -55,10 +55,10 @@ export type GetRpcFunctionFilterBuilderByArgs< : null } : // If we failed to find the function by argument, we still pass with any but also add an overridable - Fn extends never + Fn extends false ? { Row: any - Result: { error: true } & "Couldn't find function" + Result: { error: true } & "Couldn't infer function definition matching provided arguments" RelationName: FnName Relationships: null } diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 33e54862..b0fb592c 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -662,4 +662,4 @@ type FindMatchingFunctionBySetofFrom< TableName extends string > = FnUnion extends infer Fn extends GenericFunction ? MatchingFunctionBySetofFrom - : never + : false diff --git a/test/advanced_rpc.test-d.ts b/test/advanced_rpc.test-d.ts index 39f712a4..c03b0855 100644 --- a/test/advanced_rpc.test-d.ts +++ b/test/advanced_rpc.test-d.ts @@ -9,14 +9,14 @@ type Schema = Database['public'] { const { data } = await rpcQueries['function returning a setof embeded table'] let result: Exclude - let expected: Array + let expected: Array> expectType>(true) } { const { data } = await rpcQueries['function double definition returning a setof embeded table'] let result: Exclude - let expected: Array + let expected: Array> expectType>(true) } @@ -30,21 +30,21 @@ type Schema = Database['public'] { const { data } = await rpcQueries['function with scalar input'] let result: Exclude - let expected: Array + let expected: Array> expectType>(true) } { const { data } = await rpcQueries['function with table row input'] let result: Exclude - let expected: Array + let expected: Array> expectType>(true) } { const { data } = await rpcQueries['function with view row input'] let result: Exclude - let expected: Array + let expected: Array> expectType>(true) } @@ -146,14 +146,14 @@ type Schema = Database['public'] { const { data } = await rpcQueries['resolvable function with channel_id and search params'] let result: Exclude - let expected: Array + let expected: Array> expectType>(true) } { const { data } = await rpcQueries['resolvable function with user_row param'] let result: Exclude - let expected: Array + let expected: Array> expectType>(true) } @@ -287,3 +287,36 @@ type Schema = Database['public'] | 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 index c47debb0..d7249356 100644 --- a/test/advanced_rpc.ts +++ b/test/advanced_rpc.ts @@ -214,6 +214,17 @@ export const rpcQueries = { '': 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', () => { @@ -920,4 +931,97 @@ describe('rpc', () => { } `) }) + + 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 93a836fb..7734cee6 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -307,3 +307,24 @@ create or replace function public.polymorphic_function_with_unnamed_default_over 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/embeded_functions_join.test-d.ts b/test/embeded_functions_join.test-d.ts index e5d268c8..23d56f0f 100644 --- a/test/embeded_functions_join.test-d.ts +++ b/test/embeded_functions_join.test-d.ts @@ -130,3 +130,30 @@ type Schema = Database['public'] }> 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 index b4be5aa0..93b206dd 100644 --- a/test/embeded_functions_join.ts +++ b/test/embeded_functions_join.ts @@ -50,6 +50,18 @@ export const selectParams = { 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 = { @@ -89,6 +101,15 @@ export const selectQueries = { 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', () => { @@ -627,4 +648,90 @@ describe('select', () => { } `) }) + + 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/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/types.generated.ts b/test/types.generated.ts index 03dde130..ea164915 100644 --- a/test/types.generated.ts +++ b/test/types.generated.ts @@ -1,4 +1,4 @@ -export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] +export type Json = unknown export type Database = { personal: { @@ -297,6 +297,7 @@ export type Database = { id: number message: string | null username: string + blurb_message: string | null } Insert: { channel_id: number @@ -582,6 +583,26 @@ export type Database = { } } Functions: { + 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 From b4b03e709bf4e3ccd3058c877061626331b9835d Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 21 Apr 2025 16:32:08 +0200 Subject: [PATCH 15/15] chore: export GetRPC type --- src/index.ts | 1 + 1 file changed, 1 insertion(+) 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'