Skip to content

Commit 7e985d4

Browse files
authored
Merge pull request #592 from supabase/feat/add-json-path-type-inference
feat(types): add json path type inference
2 parents 633991c + 978d88d commit 7e985d4

File tree

9 files changed

+364
-82
lines changed

9 files changed

+364
-82
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/result.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
GetFieldNodeResultName,
1616
IsAny,
1717
IsRelationNullable,
18+
IsStringUnion,
19+
JsonPathToType,
1820
ResolveRelationship,
1921
SelectQueryError,
2022
} from './utils'
@@ -239,6 +241,30 @@ type ProcessFieldNode<
239241
? ProcessEmbeddedResource<Schema, Relationships, Field, RelationName>
240242
: ProcessSimpleField<Row, RelationName, Field>
241243

244+
type ResolveJsonPathType<
245+
Value,
246+
Path extends string | undefined,
247+
CastType extends PostgreSQLTypes
248+
> = Path extends string
249+
? JsonPathToType<Value, Path> extends never
250+
? // Always fallback if JsonPathToType returns never
251+
TypeScriptTypes<CastType>
252+
: JsonPathToType<Value, Path> extends infer PathResult
253+
? PathResult extends string
254+
? // Use the result if it's a string as we know that even with the string accessor ->> it's a valid type
255+
PathResult
256+
: IsStringUnion<PathResult> extends true
257+
? // Use the result if it's a union of strings
258+
PathResult
259+
: CastType extends 'json'
260+
? // If the type is not a string, ensure it was accessed with json accessor ->
261+
PathResult
262+
: // Otherwise it means non-string value accessed with string accessor ->> use the TypeScriptTypes result
263+
TypeScriptTypes<CastType>
264+
: TypeScriptTypes<CastType>
265+
: // No json path, use regular type casting
266+
TypeScriptTypes<CastType>
267+
242268
/**
243269
* Processes a simple field (without embedded resources).
244270
*
@@ -261,8 +287,8 @@ type ProcessSimpleField<
261287
}
262288
: {
263289
// Aliases override the property name in the result
264-
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes // We apply the detected casted as the result type
265-
? TypeScriptTypes<Field['castType']>
290+
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes
291+
? ResolveJsonPathType<Row[Field['name']], Field['jsonPath'], Field['castType']>
266292
: Row[Field['name']]
267293
}
268294
: SelectQueryError<`column '${Field['name']}' does not exist on '${RelationName}'.`>

src/select-query-parser/utils.ts

+34
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,37 @@ 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}` // Handle ->> operator
550+
? JsonPathToAccessor<`${P1}.${Rest}`>
551+
: P2 extends string // Handle -> operator
552+
? JsonPathToAccessor<`${P1}.${P2}`>
553+
: Path
554+
: Path extends `>${infer Rest}` // Clean up any remaining > characters
555+
? JsonPathToAccessor<Rest>
556+
: Path extends `${infer P1}::${infer _}` // Handle type casting
557+
? JsonPathToAccessor<P1>
558+
: Path extends `${infer P1}${')' | ','}${infer _}` // Handle closing parenthesis and comma
559+
? P1
560+
: Path
561+
562+
export type JsonPathToType<T, Path extends string> = Path extends ''
563+
? T
564+
: ContainsNull<T> extends true
565+
? JsonPathToType<Exclude<T, null>, Path>
566+
: Path extends `${infer Key}.${infer Rest}`
567+
? Key extends keyof T
568+
? JsonPathToType<T[Key], Rest>
569+
: never
570+
: Path extends keyof T
571+
? T[Path]
572+
: never
573+
574+
export type IsStringUnion<T> = string extends T
575+
? false
576+
: T extends string
577+
? [T] extends [never]
578+
? false
579+
: true
580+
: false

test/basic.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PostgrestClient } from '../src/index'
2-
import { Database } from './types'
2+
import { CustomUserDataType, Database } from './types'
33

44
const REST_URL = 'http://localhost:3000'
55
const postgrest = new PostgrestClient<Database>(REST_URL)
@@ -1693,7 +1693,10 @@ test('select with no match', async () => {
16931693
})
16941694

16951695
test('update with no match - return=minimal', async () => {
1696-
const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing')
1696+
const res = await postgrest
1697+
.from('users')
1698+
.update({ data: '' as unknown as CustomUserDataType })
1699+
.eq('username', 'missing')
16971700
expect(res).toMatchInlineSnapshot(`
16981701
Object {
16991702
"count": null,
@@ -1706,7 +1709,11 @@ test('update with no match - return=minimal', async () => {
17061709
})
17071710

17081711
test('update with no match - return=representation', async () => {
1709-
const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing').select()
1712+
const res = await postgrest
1713+
.from('users')
1714+
.update({ data: '' as unknown as CustomUserDataType })
1715+
.eq('username', 'missing')
1716+
.select()
17101717
expect(res).toMatchInlineSnapshot(`
17111718
Object {
17121719
"count": null,

0 commit comments

Comments
 (0)