Skip to content

Commit

Permalink
feat: introduce rpc embeding test with possible implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
avallete committed Oct 17, 2024
1 parent 2089a8b commit d52234f
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 19 deletions.
7 changes: 7 additions & 0 deletions test/db/00-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 60 additions & 8 deletions test/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Database } from './types'
const REST_URL = 'http://localhost:3000'
export const postgrest = new PostgrestClient<Database>(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,
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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",
}
`)
})
82 changes: 71 additions & 11 deletions test/select-query-parser/rpc.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
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'

// 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<typeof data, null>
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns']
expectType<TypeEqual<typeof result, typeof expected>>(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<typeof data, null>
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns']
expectType<TypeEqual<typeof result, typeof expected>>(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<typeof data, null>
let expected: { username: string }[]
Expand All @@ -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<typeof data, null>
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns']
expectType<TypeEqual<typeof result, typeof expected>>(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<typeof data, null>
let expected: { name: string }[]
Expand All @@ -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<typeof data, null>
let expected: { status: string }[]
Expand All @@ -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<typeof data, null>
let expected: { count: number; status: 'ONLINE' | 'OFFLINE' }[]
expectType<TypeEqual<typeof result, typeof expected>>(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<PropertyKey, never>
// 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: '<table-id-in-schema>' }
// Args: Record<PropertyKey, never>
// }
//
// 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<FnName extends string & keyof Schema['Functions'], Fn extends Schema['Functions'][FnName]>(
// ...
// ):
// Fn['setOf'] extends { refName: string extends keyof Schema['Tables'] }
// ? PostgrestFilterBuilder<Schema, GetTable<Fn['setOf']>['Row'], Fn['setOf']['refName'], GetTable<Fn['setOf']>['Relationships']>
// : PostgrestFilterBuilder<
// Schema,
// Fn['Returns'] extends any[]
// ? Fn['Returns'][number] extends Record<string, unknown>
// ? 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<typeof data, null>
// let expected: {
// username: string
// status: 'ONLINE' | 'OFFLINE'
// profile: { id: number }[]
// }[]
// expectType<TypeEqual<typeof result, typeof expected>>(true)
// }
10 changes: 10 additions & 0 deletions test/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,16 @@ export type Database = {
}
Returns: Database['public']['Enums']['user_status']
}
get_all_users: {
Args: Record<PropertyKey, never>
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
Expand Down

0 comments on commit d52234f

Please sign in to comment.