Skip to content

Commit d52234f

Browse files
committed
feat: introduce rpc embeding test with possible implementation
1 parent 2089a8b commit d52234f

File tree

4 files changed

+148
-19
lines changed

4 files changed

+148
-19
lines changed

test/db/00-schema.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ RETURNS TABLE(username text, status user_status) AS $$
8484
SELECT username, status from users WHERE username=name_param;
8585
$$ LANGUAGE SQL IMMUTABLE;
8686

87+
88+
create function get_all_users() returns setof users
89+
language sql stable
90+
as $$
91+
select * from users;
92+
$$;
93+
8794
CREATE FUNCTION public.offline_user(name_param text)
8895
RETURNS user_status AS $$
8996
UPDATE users SET status = 'OFFLINE' WHERE username=name_param

test/rpc.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { Database } from './types'
44
const REST_URL = 'http://localhost:3000'
55
export const postgrest = new PostgrestClient<Database>(REST_URL)
66

7-
export const RPC_NAME = 'get_username_and_status'
7+
export const RPC_NAME_SCALAR = 'get_username_and_status'
8+
export const RPC_SETOF_NAME = 'get_all_users'
89

910
export const selectParams = {
1011
noParams: undefined,
@@ -14,24 +15,32 @@ export const selectParams = {
1415
fieldAliasing: 'name:username',
1516
fieldCasting: 'status::text',
1617
fieldAggregate: 'username.count(), status',
18+
selectFieldAndEmbedRelation: 'username, status, profile:user_profiles(id)',
1719
} as const
1820

1921
export const selectQueries = {
20-
noParams: postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select(selectParams.noParams),
21-
starSelect: postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select(selectParams.starSelect),
22-
fieldSelect: postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select(selectParams.fieldSelect),
22+
noParams: postgrest.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' }).select(selectParams.noParams),
23+
starSelect: postgrest
24+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
25+
.select(selectParams.starSelect),
26+
fieldSelect: postgrest
27+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
28+
.select(selectParams.fieldSelect),
2329
fieldsSelect: postgrest
24-
.rpc(RPC_NAME, { name_param: 'supabot' })
30+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
2531
.select(selectParams.fieldsSelect),
2632
fieldAliasing: postgrest
27-
.rpc(RPC_NAME, { name_param: 'supabot' })
33+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
2834
.select(selectParams.fieldAliasing),
2935
fieldCasting: postgrest
30-
.rpc(RPC_NAME, { name_param: 'supabot' })
36+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
3137
.select(selectParams.fieldCasting),
3238
fieldAggregate: postgrest
33-
.rpc(RPC_NAME, { name_param: 'supabot' })
39+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
3440
.select(selectParams.fieldAggregate),
41+
selectFieldAndEmbedRelation: postgrest
42+
.rpc(RPC_SETOF_NAME, {})
43+
.select(selectParams.selectFieldAndEmbedRelation),
3544
} as const
3645

3746
test('RPC call with no params', async () => {
@@ -156,3 +165,46 @@ test('RPC call with field aggregate', async () => {
156165
}
157166
`)
158167
})
168+
169+
test('RPC call with select field and embed relation', async () => {
170+
const res = await selectQueries.selectFieldAndEmbedRelation
171+
expect(res).toMatchInlineSnapshot(`
172+
Object {
173+
"count": null,
174+
"data": Array [
175+
Object {
176+
"profile": Array [
177+
Object {
178+
"id": 1,
179+
},
180+
],
181+
"status": "ONLINE",
182+
"username": "supabot",
183+
},
184+
Object {
185+
"profile": Array [],
186+
"status": "OFFLINE",
187+
"username": "kiwicopple",
188+
},
189+
Object {
190+
"profile": Array [],
191+
"status": "ONLINE",
192+
"username": "awailas",
193+
},
194+
Object {
195+
"profile": Array [],
196+
"status": "ONLINE",
197+
"username": "jsonuser",
198+
},
199+
Object {
200+
"profile": Array [],
201+
"status": "ONLINE",
202+
"username": "dragarcia",
203+
},
204+
],
205+
"error": null,
206+
"status": 200,
207+
"statusText": "OK",
208+
}
209+
`)
210+
})
Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
1-
import { postgrest, selectParams, RPC_NAME } from '../rpc'
1+
import { postgrest, selectParams, RPC_NAME_SCALAR } from '../rpc'
22
import { Database } from '../types'
33
import { expectType } from 'tsd'
44
import { TypeEqual } from 'ts-expect'
55

66
// RPC call with no params
77
{
88
const { data } = await postgrest
9-
.rpc(RPC_NAME, { name_param: 'supabot' })
9+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
1010
.select(selectParams.noParams)
1111
let result: Exclude<typeof data, null>
12-
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
12+
let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns']
1313
expectType<TypeEqual<typeof result, typeof expected>>(true)
1414
}
1515

1616
// RPC call with star select
1717
{
1818
const { data } = await postgrest
19-
.rpc(RPC_NAME, { name_param: 'supabot' })
19+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
2020
.select(selectParams.starSelect)
2121
let result: Exclude<typeof data, null>
22-
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
22+
let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns']
2323
expectType<TypeEqual<typeof result, typeof expected>>(true)
2424
}
2525

2626
// RPC call with single field select
2727
{
2828
const { data } = await postgrest
29-
.rpc(RPC_NAME, { name_param: 'supabot' })
29+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
3030
.select(selectParams.fieldSelect)
3131
let result: Exclude<typeof data, null>
3232
let expected: { username: string }[]
@@ -36,17 +36,17 @@ import { TypeEqual } from 'ts-expect'
3636
// RPC call with multiple fields select
3737
{
3838
const { data } = await postgrest
39-
.rpc(RPC_NAME, { name_param: 'supabot' })
39+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
4040
.select(selectParams.fieldsSelect)
4141
let result: Exclude<typeof data, null>
42-
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
42+
let expected: Database['public']['Functions'][typeof RPC_NAME_SCALAR]['Returns']
4343
expectType<TypeEqual<typeof result, typeof expected>>(true)
4444
}
4545

4646
// RPC call with field aliasing
4747
{
4848
const { data } = await postgrest
49-
.rpc(RPC_NAME, { name_param: 'supabot' })
49+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
5050
.select(selectParams.fieldAliasing)
5151
let result: Exclude<typeof data, null>
5252
let expected: { name: string }[]
@@ -56,7 +56,7 @@ import { TypeEqual } from 'ts-expect'
5656
// RPC call with field casting
5757
{
5858
const { data } = await postgrest
59-
.rpc(RPC_NAME, { name_param: 'supabot' })
59+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
6060
.select(selectParams.fieldCasting)
6161
let result: Exclude<typeof data, null>
6262
let expected: { status: string }[]
@@ -66,9 +66,69 @@ import { TypeEqual } from 'ts-expect'
6666
// RPC call with field aggregate
6767
{
6868
const { data } = await postgrest
69-
.rpc(RPC_NAME, { name_param: 'supabot' })
69+
.rpc(RPC_NAME_SCALAR, { name_param: 'supabot' })
7070
.select(selectParams.fieldAggregate)
7171
let result: Exclude<typeof data, null>
7272
let expected: { count: number; status: 'ONLINE' | 'OFFLINE' }[]
7373
expectType<TypeEqual<typeof result, typeof expected>>(true)
7474
}
75+
76+
// RPC call with select field and embed relation
77+
// TODO: Implement support for RPC functions that return a set of rows
78+
//
79+
// Current introspection for functions returning setof:
80+
// get_all_users: {
81+
// Args: Record<PropertyKey, never>
82+
// Returns: {
83+
// age_range: unknown | null
84+
// catchphrase: unknown | null
85+
// data: Json | null
86+
// status: Database["public"]["Enums"]["user_status"] | null
87+
// username: string
88+
// }[]
89+
// }
90+
//
91+
// Proposed introspection change:
92+
// get_all_users: {
93+
// setOf?: { refName: '<table-id-in-schema>' }
94+
// Args: Record<PropertyKey, never>
95+
// }
96+
//
97+
// This would allow for proper typing of RPC calls that return sets,
98+
// enabling them to use the same filtering and selection capabilities
99+
// as table queries.
100+
//
101+
// On the PostgrestClient side, the rpc method should be updated to
102+
// handle the 'setOf' property, branching the return type based on its presence:
103+
//
104+
// rpc<FnName extends string & keyof Schema['Functions'], Fn extends Schema['Functions'][FnName]>(
105+
// ...
106+
// ):
107+
// Fn['setOf'] extends { refName: string extends keyof Schema['Tables'] }
108+
// ? PostgrestFilterBuilder<Schema, GetTable<Fn['setOf']>['Row'], Fn['setOf']['refName'], GetTable<Fn['setOf']>['Relationships']>
109+
// : PostgrestFilterBuilder<
110+
// Schema,
111+
// Fn['Returns'] extends any[]
112+
// ? Fn['Returns'][number] extends Record<string, unknown>
113+
// ? Fn['Returns'][number]
114+
// : never
115+
// : never,
116+
// Fn['Returns'],
117+
// FnName,
118+
// null
119+
// >
120+
//
121+
// Implementation can be done in a follow-up PR.
122+
123+
// {
124+
// const { data } = await postgrest
125+
// .rpc(RPC_SETOF_NAME, {})
126+
// .select(selectParams.selectFieldAndEmbedRelation)
127+
// let result: Exclude<typeof data, null>
128+
// let expected: {
129+
// username: string
130+
// status: 'ONLINE' | 'OFFLINE'
131+
// profile: { id: number }[]
132+
// }[]
133+
// expectType<TypeEqual<typeof result, typeof expected>>(true)
134+
// }

test/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,16 @@ export type Database = {
372372
}
373373
Returns: Database['public']['Enums']['user_status']
374374
}
375+
get_all_users: {
376+
Args: Record<PropertyKey, never>
377+
Returns: {
378+
age_range: unknown | null
379+
catchphrase: unknown | null
380+
data: Json | null
381+
status: Database['public']['Enums']['user_status'] | null
382+
username: string
383+
}[]
384+
}
375385
get_username_and_status: {
376386
Args: {
377387
name_param: string

0 commit comments

Comments
 (0)