diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index c06ed0b7..46702f09 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -84,6 +84,13 @@ RETURNS TABLE(username text, status user_status) AS $$ SELECT username, status from users WHERE username=name_param; $$ LANGUAGE SQL IMMUTABLE; + +create function get_all_users() returns setof users +language sql stable +as $$ + select * from users; +$$; + CREATE FUNCTION public.offline_user(name_param text) RETURNS user_status AS $$ UPDATE users SET status = 'OFFLINE' WHERE username=name_param diff --git a/test/rpc.ts b/test/rpc.ts index 8e1e263f..e01657c5 100644 --- a/test/rpc.ts +++ b/test/rpc.ts @@ -4,7 +4,8 @@ import { Database } from './types' const REST_URL = 'http://localhost:3000' export const postgrest = new PostgrestClient(REST_URL) -export const RPC_NAME = 'get_username_and_status' +export const RPC_NAME_SCALAR = 'get_username_and_status' +export const RPC_SETOF_NAME = 'get_all_users' export const selectParams = { noParams: undefined, @@ -14,24 +15,32 @@ export const selectParams = { fieldAliasing: 'name:username', fieldCasting: 'status::text', fieldAggregate: 'username.count(), status', + selectFieldAndEmbedRelation: 'username, status, profile:user_profiles(id)', } as const export const selectQueries = { - noParams: postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select(selectParams.noParams), - starSelect: postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select(selectParams.starSelect), - fieldSelect: postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select(selectParams.fieldSelect), + noParams: postgrest.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }).select(selectParams.noParams), + starSelect: postgrest + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) + .select(selectParams.starSelect), + fieldSelect: postgrest + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) + .select(selectParams.fieldSelect), fieldsSelect: postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldsSelect), fieldAliasing: postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldAliasing), fieldCasting: postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldCasting), fieldAggregate: postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldAggregate), + selectFieldAndEmbedRelation: postgrest + .rpc(RPC_SETOF_NAME, {}) + .select(selectParams.selectFieldAndEmbedRelation), } as const test('RPC call with no params', async () => { @@ -156,3 +165,46 @@ test('RPC call with field aggregate', async () => { } `) }) + +test('RPC call with select field and embed relation', async () => { + const res = await selectQueries.selectFieldAndEmbedRelation + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "profile": Array [ + Object { + "id": 1, + }, + ], + "status": "ONLINE", + "username": "supabot", + }, + Object { + "profile": Array [], + "status": "OFFLINE", + "username": "kiwicopple", + }, + Object { + "profile": Array [], + "status": "ONLINE", + "username": "awailas", + }, + Object { + "profile": Array [], + "status": "ONLINE", + "username": "jsonuser", + }, + Object { + "profile": Array [], + "status": "ONLINE", + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) diff --git a/test/select-query-parser/rpc.test-d.ts b/test/select-query-parser/rpc.test-d.ts index 858434bf..92e02fd0 100644 --- a/test/select-query-parser/rpc.test-d.ts +++ b/test/select-query-parser/rpc.test-d.ts @@ -1,4 +1,4 @@ -import { postgrest, selectParams, RPC_NAME } from '../rpc' +import { postgrest, selectParams, RPC_NAME_SCALAR } from '../rpc' import { Database } from '../types' import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' @@ -6,27 +6,27 @@ import { TypeEqual } from 'ts-expect' // RPC call with no params { const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.noParams) let result: Exclude - let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns'] + let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns'] expectType>(true) } // RPC call with star select { const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.starSelect) let result: Exclude - let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns'] + let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns'] expectType>(true) } // RPC call with single field select { const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldSelect) let result: Exclude let expected: { username: string }[] @@ -36,17 +36,17 @@ import { TypeEqual } from 'ts-expect' // RPC call with multiple fields select { const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldsSelect) let result: Exclude - let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns'] + let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns'] expectType>(true) } // RPC call with field aliasing { const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldAliasing) let result: Exclude let expected: { name: string }[] @@ -56,7 +56,7 @@ import { TypeEqual } from 'ts-expect' // RPC call with field casting { const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldCasting) let result: Exclude let expected: { status: string }[] @@ -66,9 +66,69 @@ import { TypeEqual } from 'ts-expect' // RPC call with field aggregate { const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) + .rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }) .select(selectParams.fieldAggregate) let result: Exclude let expected: { count: number; status: 'ONLINE' | 'OFFLINE' }[] expectType>(true) } + +// RPC call with select field and embed relation +// TODO: Implement support for RPC functions that return a set of rows +// +// Current introspection for functions returning setof: +// get_all_users: { +// Args: Record +// Returns: { +// age_range: unknown | null +// catchphrase: unknown | null +// data: Json | null +// status: Database["public"]["Enums"]["user_status"] | null +// username: string +// }[] +// } +// +// Proposed introspection change: +// get_all_users: { +// setOf?: { refName: '' } +// Args: Record +// } +// +// This would allow for proper typing of RPC calls that return sets, +// enabling them to use the same filtering and selection capabilities +// as table queries. +// +// On the PostgrestClient side, the rpc method should be updated to +// handle the 'setOf' property, branching the return type based on its presence: +// +// rpc( +// ... +// ): +// Fn['setOf'] extends { refName: string extends keyof Schema['Tables'] } +// ? PostgrestFilterBuilder['Row'], Fn['setOf']['refName'], GetTable['Relationships']> +// : PostgrestFilterBuilder< +// Schema, +// Fn['Returns'] extends any[] +// ? Fn['Returns'][number] extends Record +// ? Fn['Returns'][number] +// : never +// : never, +// Fn['Returns'], +// FnName, +// null +// > +// +// Implementation can be done in a follow-up PR. + +// { +// const { data } = await postgrest +// .rpc(RPC_SETOF_NAME, {}) +// .select(selectParams.selectFieldAndEmbedRelation) +// let result: Exclude +// let expected: { +// username: string +// status: 'ONLINE' | 'OFFLINE' +// profile: { id: number }[] +// }[] +// expectType>(true) +// } diff --git a/test/types.ts b/test/types.ts index 2d601877..1ac335c7 100644 --- a/test/types.ts +++ b/test/types.ts @@ -372,6 +372,16 @@ export type Database = { } Returns: Database['public']['Enums']['user_status'] } + get_all_users: { + Args: Record + Returns: { + age_range: unknown | null + catchphrase: unknown | null + data: Json | null + status: Database['public']['Enums']['user_status'] | null + username: string + }[] + } get_username_and_status: { Args: { name_param: string