Skip to content

Commit 8bdb969

Browse files
committed
feat(types): add jsonpath parser
1 parent 8d32089 commit 8bdb969

File tree

5 files changed

+76
-13
lines changed

5 files changed

+76
-13
lines changed

src/select-query-parser/parser.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284
33

44
import { SimplifyDeep } from '../types'
5+
import { JsonPathToAccessor } from './utils'
56

67
/**
78
* Parses a query.
@@ -220,13 +221,24 @@ type ParseNonEmbeddedResourceField<Input extends string> = ParseIdentifier<Input
220221
]
221222
? // Parse optional JSON path.
222223
(
223-
Remainder extends `->${infer _}`
224+
Remainder extends `->${infer PathAndRest}`
224225
? ParseJsonAccessor<Remainder> extends [
225226
infer PropertyName,
226227
infer PropertyType,
227228
`${infer Remainder}`
228229
]
229-
? [{ type: 'field'; name: Name; alias: PropertyName; castType: PropertyType }, Remainder]
230+
? [
231+
{
232+
type: 'field'
233+
name: Name
234+
alias: PropertyName
235+
castType: PropertyType
236+
jsonPath: JsonPathToAccessor<
237+
PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest
238+
>
239+
},
240+
Remainder
241+
]
230242
: ParseJsonAccessor<Remainder>
231243
: [{ type: 'field'; name: Name }, Remainder]
232244
) extends infer Parsed
@@ -401,6 +413,7 @@ export namespace Ast {
401413
hint?: string
402414
innerJoin?: true
403415
castType?: string
416+
jsonPath?: string
404417
aggregateFunction?: Token.AggregateFunction
405418
children?: Node[]
406419
}

src/select-query-parser/utils.ts

+8
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,11 @@ export type FindFieldMatchingRelationships<
544544
name: Field['name']
545545
}
546546
: SelectQueryError<'Failed to find matching relation via name'>
547+
548+
export type JsonPathToAccessor<Path extends string> = Path extends `${infer P1}->${infer P2}`
549+
? P2 extends `>${infer Rest}` // Check if P2 starts with > (from ->>)
550+
? JsonPathToAccessor<`${P1}.${Rest}`>
551+
: JsonPathToAccessor<`${P1}.${P2}`>
552+
: Path extends `${infer P1}::${infer _}` // Handle type casting
553+
? JsonPathToAccessor<P1> // Process the path without the cast type
554+
: Path

test/index.test-d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,14 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
211211
expectType<typeof x>(y)
212212
expectType<typeof x>(z)
213213
}
214+
215+
// Json Accessor with custom types overrides
216+
{
217+
const { error } = await postgrest
218+
.schema('personal')
219+
.from('users')
220+
.select('data->foo->bar, data->foo->>baz')
221+
if (error) {
222+
throw new Error(error.message)
223+
}
224+
}

test/select-query-parser/parser.test-d.ts

+31-8
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,28 @@ import { selectParams } from '../relationships'
8181
// Select with JSON accessor
8282
{
8383
expectType<ParseQuery<'data->preferences->theme'>>([
84-
{ type: 'field', name: 'data', alias: 'theme', castType: 'json' },
84+
{
85+
type: 'field',
86+
name: 'data',
87+
alias: 'theme',
88+
castType: 'json',
89+
jsonPath: 'preferences.theme',
90+
},
8591
])
8692
}
8793

8894
// Select with JSON accessor and text conversion
8995
{
9096
expectType<ParseQuery<'data->preferences->>theme'>>([
91-
{ type: 'field', name: 'data', alias: 'theme', castType: 'text' },
97+
{
98+
type: 'field',
99+
name: 'data',
100+
alias: 'theme',
101+
castType: 'text',
102+
jsonPath: 'preferences.theme',
103+
},
92104
])
93105
}
94-
95106
// Select with spread
96107
{
97108
expectType<ParseQuery<'username, ...posts(id, title)'>>([
@@ -196,7 +207,13 @@ import { selectParams } from '../relationships'
196207
},
197208
],
198209
},
199-
{ type: 'field', name: 'profile', alias: 'theme', castType: 'text' },
210+
{
211+
type: 'field',
212+
name: 'profile',
213+
alias: 'theme',
214+
castType: 'text',
215+
jsonPath: 'settings.theme',
216+
},
200217
])
201218
}
202219
{
@@ -327,7 +344,13 @@ import { selectParams } from '../relationships'
327344
// Select with nested JSON accessors
328345
{
329346
expectType<ParseQuery<'data->preferences->theme->color'>>([
330-
{ type: 'field', name: 'data', alias: 'color', castType: 'json' },
347+
{
348+
type: 'field',
349+
name: 'data',
350+
alias: 'color',
351+
castType: 'json',
352+
jsonPath: 'preferences.theme.color',
353+
},
331354
])
332355
}
333356

@@ -464,7 +487,7 @@ import { selectParams } from '../relationships'
464487
expectType<ParseQuery<'id::text, created_at::date, data->age::int'>>([
465488
{ type: 'field', name: 'id', castType: 'text' },
466489
{ type: 'field', name: 'created_at', castType: 'date' },
467-
{ type: 'field', name: 'data', alias: 'age', castType: 'int' },
490+
{ type: 'field', name: 'data', alias: 'age', castType: 'int', jsonPath: 'age' },
468491
])
469492
}
470493

@@ -480,8 +503,8 @@ import { selectParams } from '../relationships'
480503
// select JSON accessor
481504
{
482505
expect<ParseQuery<typeof selectParams.selectJsonAccessor.select>>([
483-
{ type: 'field', name: 'data', alias: 'bar', castType: 'json' },
484-
{ type: 'field', name: 'data', alias: 'baz', castType: 'text' },
506+
{ type: 'field', name: 'data', alias: 'bar', castType: 'json', jsonPath: 'foo.bar' },
507+
{ type: 'field', name: 'data', alias: 'baz', castType: 'text', jsonPath: 'foo.baz' },
485508
])
486509
}
487510

test/types.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
11
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
22

3+
type CustomUserDataType = {
4+
foo: string
5+
bar: {
6+
baz: number
7+
}
8+
en: 'ONE' | 'TWO' | 'THREE'
9+
}
10+
311
export type Database = {
412
personal: {
513
Tables: {
614
users: {
715
Row: {
816
age_range: unknown | null
9-
data: Json | null
17+
data: CustomUserDataType | null
1018
status: Database['public']['Enums']['user_status'] | null
1119
username: string
1220
}
1321
Insert: {
1422
age_range?: unknown | null
15-
data?: Json | null
23+
data?: CustomUserDataType | null
1624
status?: Database['public']['Enums']['user_status'] | null
1725
username: string
1826
}
1927
Update: {
2028
age_range?: unknown | null
21-
data?: Json | null
29+
data?: CustomUserDataType | null
2230
status?: Database['public']['Enums']['user_status'] | null
2331
username?: string
2432
}

0 commit comments

Comments
 (0)