Skip to content

Commit a0e4db7

Browse files
authored
feat: support ->/->> for column names
1 parent 281ffe0 commit a0e4db7

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

src/select-query-parser.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@ type Digit = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0'
3838

3939
type Letter = Alphabet | Digit | '_'
4040

41+
type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
42+
4143
// /**
4244
// * Parsed node types.
4345
// * Currently only `*` and all other fields.
4446
// */
4547
// type ParsedNode =
4648
// | { star: true }
4749
// | { name: string; original: string }
48-
// | { name: string; foreignTable: true };
50+
// | { name: string; foreignTable: true }
51+
// | { name: string; type: T };
4952

5053
/**
5154
* Parser errors.
@@ -90,6 +93,8 @@ type ConstructFieldDefinition<
9093
}
9194
: Field extends { name: string; original: string }
9295
? { [K in Field['name']]: Row[Field['original']] }
96+
: Field extends { name: string; type: infer T }
97+
? { [K in Field['name']]: T }
9398
: Record<string, unknown>
9499

95100
/**
@@ -131,17 +136,19 @@ type ParseIdentifier<Input extends string> = ReadLetters<Input>
131136
* A node is one of the following:
132137
* - `*`
133138
* - `field`
139+
* - `field->json...`
134140
* - `field(nodes)`
135141
* - `field!hint(nodes)`
136142
* - `field!inner(nodes)`
137143
* - `field!hint!inner(nodes)`
138144
* - `renamed_field:field`
145+
* - `renamed_field:field->json...`
139146
* - `renamed_field:field(nodes)`
140147
* - `renamed_field:field!hint(nodes)`
141148
* - `renamed_field:field!inner(nodes)`
142149
* - `renamed_field:field!hint!inner(nodes)`
143150
*
144-
* TODO: casting operators `::text`, JSON operators `->`, `->>`.
151+
* TODO: casting operators `::text`, more support for JSON operators `->`, `->>`.
145152
*/
146153
type ParseNode<Input extends string> = Input extends ''
147154
? ParserError<'Empty string'>
@@ -225,6 +232,13 @@ type ParseNode<Input extends string> = Input extends ''
225232
]
226233
? // `renamed_field:field(nodes)`
227234
[{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
235+
: ParseJsonAccessor<EatWhitespace<Remainder>> extends [
236+
infer _PropertyName,
237+
infer PropertyType,
238+
`${infer Remainder}`
239+
]
240+
? // `renamed_field:field->json...`
241+
[{ name: Name; type: PropertyType }, EatWhitespace<Remainder>]
228242
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
229243
? ParseEmbeddedResource<EatWhitespace<Remainder>>
230244
: // `renamed_field:field`
@@ -233,12 +247,42 @@ type ParseNode<Input extends string> = Input extends ''
233247
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [infer Fields, `${infer Remainder}`]
234248
? // `field(nodes)`
235249
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
250+
: ParseJsonAccessor<EatWhitespace<Remainder>> extends [
251+
infer PropertyName,
252+
infer PropertyType,
253+
`${infer Remainder}`
254+
]
255+
? // `field->json...`
256+
[{ name: PropertyName; type: PropertyType }, EatWhitespace<Remainder>]
236257
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
237258
? ParseEmbeddedResource<EatWhitespace<Remainder>>
238259
: // `field`
239260
[{ name: Name; original: Name }, EatWhitespace<Remainder>]
240261
: ParserError<`Expected identifier at \`${Input}\``>
241262

263+
/**
264+
* Parses a JSON property accessor of the shape `->a->b->c`. The last accessor in
265+
* the series may convert to text by using the ->> operator instead of ->.
266+
*
267+
* Returns a tuple of ["Last property name", "Last property type", "Remainder of text"]
268+
* or the original string input indicating that no opening `->` was found.
269+
*/
270+
type ParseJsonAccessor<Input extends string> = Input extends `->${infer Remainder}`
271+
? Remainder extends `>${infer Remainder}`
272+
? ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
273+
? [Name, string, EatWhitespace<Remainder>]
274+
: ParserError<'Expected property name after `->>`'>
275+
: ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
276+
? ParseJsonAccessor<Remainder> extends [
277+
infer PropertyName,
278+
infer PropertyType,
279+
`${infer Remainder}`
280+
]
281+
? [PropertyName, PropertyType, EatWhitespace<Remainder>]
282+
: [Name, Json, EatWhitespace<Remainder>]
283+
: ParserError<'Expected property name after `->`'>
284+
: Input
285+
242286
/**
243287
* Parses an embedded resource, which is an opening `(`, followed by a sequence of
244288
* nodes, separated by `,`, then a closing `)`.

test/index.test-d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expectError, expectType } from 'tsd'
22
import { PostgrestClient } from '../src/index'
3-
import { Database } from './types'
3+
import { Database, Json } from './types'
44

55
const REST_URL = 'http://localhost:3000'
66
const postgrest = new PostgrestClient<Database>(REST_URL)
@@ -42,3 +42,15 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
4242
{
4343
expectError(postgrest.from('updatable_view').update({ non_updatable_column: 0 }))
4444
}
45+
46+
// json accessor in select query
47+
{
48+
const { data, error } = await postgrest
49+
.from('users')
50+
.select('data->foo->bar, data->foo->>baz')
51+
.single()
52+
if (error) {
53+
throw new Error(error.message)
54+
}
55+
expectType<{ bar: Json } & { baz: string }>(data)
56+
}

0 commit comments

Comments
 (0)