Skip to content

Commit 4adf2a5

Browse files
committed
feat: handle select over RPC calls
1 parent e7b35e9 commit 4adf2a5

File tree

5 files changed

+260
-10
lines changed

5 files changed

+260
-10
lines changed

src/PostgrestClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export default class PostgrestClient<
141141
: never
142142
: never,
143143
Fn['Returns'],
144-
null,
144+
FnName,
145145
null
146146
> {
147147
let method: 'HEAD' | 'GET' | 'POST'

src/select-query-parser/result.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export type GetResult<
3434
Relationships,
3535
Query extends string
3636
> = Relationships extends null // For .rpc calls the passed relationships will be null in that case, the result will always be the function return type
37-
? Row
37+
? ParseQuery<Query> extends infer ParsedQuery extends Ast.Node[]
38+
? RPCCallNodes<ParsedQuery, RelationName extends string ? RelationName : 'rpc_call', Row>
39+
: Row
3840
: ParseQuery<Query> extends infer ParsedQuery
3941
? ParsedQuery extends Ast.Node[]
4042
? RelationName extends string
@@ -45,6 +47,40 @@ export type GetResult<
4547
: ParsedQuery
4648
: never
4749

50+
/**
51+
* Processes a single Node from a select chained after a rpc call
52+
*
53+
* @param Row - The type of a row in the current table.
54+
* @param RelationName - The name of the current rpc function
55+
* @param NodeType - The Node to process.
56+
*/
57+
export type ProcessRPCNode<
58+
Row extends Record<string, unknown>,
59+
RelationName extends string,
60+
NodeType extends Ast.Node
61+
> = NodeType extends Ast.StarNode // If the selection is *
62+
? Row
63+
: NodeType extends Ast.FieldNode
64+
? ProcessSimpleField<Row, RelationName, NodeType>
65+
: SelectQueryError<'Unsupported node type.'>
66+
/**
67+
* Process select call that can be chained after an rpc call
68+
*/
69+
export type RPCCallNodes<
70+
Nodes extends Ast.Node[],
71+
RelationName extends string,
72+
Row extends Record<string, unknown>,
73+
Acc extends Record<string, unknown> = {} // Acc is now an object
74+
> = Nodes extends [infer FirstNode extends Ast.Node, ...infer RestNodes extends Ast.Node[]]
75+
? ProcessRPCNode<Row, RelationName, FirstNode> extends infer FieldResult
76+
? FieldResult extends Record<string, unknown>
77+
? RPCCallNodes<RestNodes, RelationName, Row, Acc & FieldResult>
78+
: FieldResult extends SelectQueryError<infer E>
79+
? SelectQueryError<E>
80+
: SelectQueryError<'Could not retrieve a valid record or error value'>
81+
: SelectQueryError<'Processing node failed.'>
82+
: Prettify<Acc>
83+
4884
/**
4985
* Recursively processes an array of Nodes and accumulates the resulting TypeScript type.
5086
*

test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import './relationships'
33
import './filters'
44
import './resource-embedding'
55
import './transforms'
6+
import './rpc'

test/rpc.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { PostgrestClient } from '../src/index'
2+
import { Database } from './types'
3+
4+
const REST_URL = 'http://localhost:3000'
5+
export const postgrest = new PostgrestClient<Database>(REST_URL)
6+
7+
export const RPC_NAME = 'get_username_and_status'
8+
9+
export const selectParams = {
10+
noParams: undefined,
11+
starSelect: '*',
12+
fieldSelect: 'username',
13+
fieldsSelect: 'username, status',
14+
fieldAliasing: 'name:username',
15+
fieldCasting: 'status::text',
16+
fieldAggregate: 'username.count(), status',
17+
} as const
18+
19+
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),
23+
fieldsSelect: postgrest
24+
.rpc(RPC_NAME, { name_param: 'supabot' })
25+
.select(selectParams.fieldsSelect),
26+
fieldAliasing: postgrest
27+
.rpc(RPC_NAME, { name_param: 'supabot' })
28+
.select(selectParams.fieldAliasing),
29+
fieldCasting: postgrest
30+
.rpc(RPC_NAME, { name_param: 'supabot' })
31+
.select(selectParams.fieldCasting),
32+
fieldAggregate: postgrest
33+
.rpc(RPC_NAME, { name_param: 'supabot' })
34+
.select(selectParams.fieldAggregate),
35+
} as const
36+
37+
test('RPC call with no params', async () => {
38+
const res = await selectQueries.noParams
39+
expect(res).toMatchInlineSnapshot(`
40+
Object {
41+
"count": null,
42+
"data": Array [
43+
Object {
44+
"status": "ONLINE",
45+
"username": "supabot",
46+
},
47+
],
48+
"error": null,
49+
"status": 200,
50+
"statusText": "OK",
51+
}
52+
`)
53+
})
54+
55+
test('RPC call with star select', async () => {
56+
const res = await selectQueries.starSelect
57+
expect(res).toMatchInlineSnapshot(`
58+
Object {
59+
"count": null,
60+
"data": Array [
61+
Object {
62+
"status": "ONLINE",
63+
"username": "supabot",
64+
},
65+
],
66+
"error": null,
67+
"status": 200,
68+
"statusText": "OK",
69+
}
70+
`)
71+
})
72+
73+
test('RPC call with single field select', async () => {
74+
const res = await selectQueries.fieldSelect
75+
expect(res).toMatchInlineSnapshot(`
76+
Object {
77+
"count": null,
78+
"data": Array [
79+
Object {
80+
"username": "supabot",
81+
},
82+
],
83+
"error": null,
84+
"status": 200,
85+
"statusText": "OK",
86+
}
87+
`)
88+
})
89+
90+
test('RPC call with multiple fields select', async () => {
91+
const res = await selectQueries.fieldsSelect
92+
expect(res).toMatchInlineSnapshot(`
93+
Object {
94+
"count": null,
95+
"data": Array [
96+
Object {
97+
"status": "ONLINE",
98+
"username": "supabot",
99+
},
100+
],
101+
"error": null,
102+
"status": 200,
103+
"statusText": "OK",
104+
}
105+
`)
106+
})
107+
108+
test('RPC call with field aliasing', async () => {
109+
const res = await selectQueries.fieldAliasing
110+
expect(res).toMatchInlineSnapshot(`
111+
Object {
112+
"count": null,
113+
"data": Array [
114+
Object {
115+
"name": "supabot",
116+
},
117+
],
118+
"error": null,
119+
"status": 200,
120+
"statusText": "OK",
121+
}
122+
`)
123+
})
124+
125+
test('RPC call with field casting', async () => {
126+
const res = await selectQueries.fieldCasting
127+
expect(res).toMatchInlineSnapshot(`
128+
Object {
129+
"count": null,
130+
"data": Array [
131+
Object {
132+
"status": "ONLINE",
133+
},
134+
],
135+
"error": null,
136+
"status": 200,
137+
"statusText": "OK",
138+
}
139+
`)
140+
})
141+
142+
test('RPC call with field aggregate', async () => {
143+
const res = await selectQueries.fieldAggregate
144+
expect(res).toMatchInlineSnapshot(`
145+
Object {
146+
"count": null,
147+
"data": Array [
148+
Object {
149+
"count": 1,
150+
"status": "ONLINE",
151+
},
152+
],
153+
"error": null,
154+
"status": 200,
155+
"statusText": "OK",
156+
}
157+
`)
158+
})
Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,74 @@
1-
import { postgrest } from '../relationships'
1+
import { postgrest, selectParams, RPC_NAME } from '../rpc'
22
import { Database } from '../types'
33
import { expectType } from 'tsd'
44
import { TypeEqual } from 'ts-expect'
55

6-
// rpc call result in the proper type
6+
// RPC call with no params
77
{
8-
const { data } = await postgrest.rpc('get_username_and_status').select().single()
8+
const { data } = await postgrest
9+
.rpc(RPC_NAME, { name_param: 'supabot' })
10+
.select(selectParams.noParams)
911
let result: Exclude<typeof data, null>
10-
let expected: Database['public']['Functions']['get_username_and_status']['Returns'][number]
12+
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
1113
expectType<TypeEqual<typeof result, typeof expected>>(true)
1214
}
1315

14-
// select on an rpc call
16+
// RPC call with star select
1517
{
16-
const { data, error } = await postgrest.rpc('get_username_and_status').select('username')
17-
if (error) throw error
18-
expectType<{ username: string }[]>(data)
18+
const { data } = await postgrest
19+
.rpc(RPC_NAME, { name_param: 'supabot' })
20+
.select(selectParams.starSelect)
21+
let result: Exclude<typeof data, null>
22+
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
23+
expectType<TypeEqual<typeof result, typeof expected>>(true)
24+
}
25+
26+
// RPC call with single field select
27+
{
28+
const { data } = await postgrest
29+
.rpc(RPC_NAME, { name_param: 'supabot' })
30+
.select(selectParams.fieldSelect)
31+
let result: Exclude<typeof data, null>
32+
let expected: { username: string }[]
33+
expectType<TypeEqual<typeof result, typeof expected>>(true)
34+
}
35+
36+
// RPC call with multiple fields select
37+
{
38+
const { data } = await postgrest
39+
.rpc(RPC_NAME, { name_param: 'supabot' })
40+
.select(selectParams.fieldsSelect)
41+
let result: Exclude<typeof data, null>
42+
let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns']
43+
expectType<TypeEqual<typeof result, typeof expected>>(true)
44+
}
45+
46+
// RPC call with field aliasing
47+
{
48+
const { data } = await postgrest
49+
.rpc(RPC_NAME, { name_param: 'supabot' })
50+
.select(selectParams.fieldAliasing)
51+
let result: Exclude<typeof data, null>
52+
let expected: { name: string }[]
53+
expectType<TypeEqual<typeof result, typeof expected>>(true)
54+
}
55+
56+
// RPC call with field casting
57+
{
58+
const { data } = await postgrest
59+
.rpc(RPC_NAME, { name_param: 'supabot' })
60+
.select(selectParams.fieldCasting)
61+
let result: Exclude<typeof data, null>
62+
let expected: { status: string }[]
63+
expectType<TypeEqual<typeof result, typeof expected>>(true)
64+
}
65+
66+
// RPC call with field aggregate
67+
{
68+
const { data } = await postgrest
69+
.rpc(RPC_NAME, { name_param: 'supabot' })
70+
.select(selectParams.fieldAggregate)
71+
let result: Exclude<typeof data, null>
72+
let expected: { count: number; status: 'ONLINE' | 'OFFLINE' }[]
73+
expectType<TypeEqual<typeof result, typeof expected>>(true)
1974
}

0 commit comments

Comments
 (0)