From 8bdb9691cd2ec4ce9fd9227ed6abfcf3af1bf98c Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 00:07:59 +0900 Subject: [PATCH 1/5] feat(types): add jsonpath parser --- src/select-query-parser/parser.ts | 17 ++++++++-- src/select-query-parser/utils.ts | 8 +++++ test/index.test-d.ts | 11 +++++++ test/select-query-parser/parser.test-d.ts | 39 ++++++++++++++++++----- test/types.ts | 14 ++++++-- 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/select-query-parser/parser.ts b/src/select-query-parser/parser.ts index a3944d73..c4336fe4 100644 --- a/src/select-query-parser/parser.ts +++ b/src/select-query-parser/parser.ts @@ -2,6 +2,7 @@ // See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284 import { SimplifyDeep } from '../types' +import { JsonPathToAccessor } from './utils' /** * Parses a query. @@ -220,13 +221,24 @@ type ParseNonEmbeddedResourceField = ParseIdentifier${infer _}` + Remainder extends `->${infer PathAndRest}` ? ParseJsonAccessor extends [ infer PropertyName, infer PropertyType, `${infer Remainder}` ] - ? [{ type: 'field'; name: Name; alias: PropertyName; castType: PropertyType }, Remainder] + ? [ + { + type: 'field' + name: Name + alias: PropertyName + castType: PropertyType + jsonPath: JsonPathToAccessor< + PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest + > + }, + Remainder + ] : ParseJsonAccessor : [{ type: 'field'; name: Name }, Remainder] ) extends infer Parsed @@ -401,6 +413,7 @@ export namespace Ast { hint?: string innerJoin?: true castType?: string + jsonPath?: string aggregateFunction?: Token.AggregateFunction children?: Node[] } diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index b3691e93..14be31d4 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -544,3 +544,11 @@ export type FindFieldMatchingRelationships< name: Field['name'] } : SelectQueryError<'Failed to find matching relation via name'> + +export type JsonPathToAccessor = Path extends `${infer P1}->${infer P2}` + ? P2 extends `>${infer Rest}` // Check if P2 starts with > (from ->>) + ? JsonPathToAccessor<`${P1}.${Rest}`> + : JsonPathToAccessor<`${P1}.${P2}`> + : Path extends `${infer P1}::${infer _}` // Handle type casting + ? JsonPathToAccessor // Process the path without the cast type + : Path diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 50a2444d..3e318615 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -211,3 +211,14 @@ const postgrest = new PostgrestClient(REST_URL) expectType(y) expectType(z) } + +// Json Accessor with custom types overrides +{ + const { error } = await postgrest + .schema('personal') + .from('users') + .select('data->foo->bar, data->foo->>baz') + if (error) { + throw new Error(error.message) + } +} diff --git a/test/select-query-parser/parser.test-d.ts b/test/select-query-parser/parser.test-d.ts index 8c7291ac..51789a99 100644 --- a/test/select-query-parser/parser.test-d.ts +++ b/test/select-query-parser/parser.test-d.ts @@ -81,17 +81,28 @@ import { selectParams } from '../relationships' // Select with JSON accessor { expectTypepreferences->theme'>>([ - { type: 'field', name: 'data', alias: 'theme', castType: 'json' }, + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'json', + jsonPath: 'preferences.theme', + }, ]) } // Select with JSON accessor and text conversion { expectTypepreferences->>theme'>>([ - { type: 'field', name: 'data', alias: 'theme', castType: 'text' }, + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'text', + jsonPath: 'preferences.theme', + }, ]) } - // Select with spread { expectType>([ @@ -196,7 +207,13 @@ import { selectParams } from '../relationships' }, ], }, - { type: 'field', name: 'profile', alias: 'theme', castType: 'text' }, + { + type: 'field', + name: 'profile', + alias: 'theme', + castType: 'text', + jsonPath: 'settings.theme', + }, ]) } { @@ -327,7 +344,13 @@ import { selectParams } from '../relationships' // Select with nested JSON accessors { expectTypepreferences->theme->color'>>([ - { type: 'field', name: 'data', alias: 'color', castType: 'json' }, + { + type: 'field', + name: 'data', + alias: 'color', + castType: 'json', + jsonPath: 'preferences.theme.color', + }, ]) } @@ -464,7 +487,7 @@ import { selectParams } from '../relationships' expectTypeage::int'>>([ { type: 'field', name: 'id', castType: 'text' }, { type: 'field', name: 'created_at', castType: 'date' }, - { type: 'field', name: 'data', alias: 'age', castType: 'int' }, + { type: 'field', name: 'data', alias: 'age', castType: 'int', jsonPath: 'age' }, ]) } @@ -480,8 +503,8 @@ import { selectParams } from '../relationships' // select JSON accessor { expect>([ - { type: 'field', name: 'data', alias: 'bar', castType: 'json' }, - { type: 'field', name: 'data', alias: 'baz', castType: 'text' }, + { type: 'field', name: 'data', alias: 'bar', castType: 'json', jsonPath: 'foo.bar' }, + { type: 'field', name: 'data', alias: 'baz', castType: 'text', jsonPath: 'foo.baz' }, ]) } diff --git a/test/types.ts b/test/types.ts index 2ed1d1c4..7a2954ef 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,24 +1,32 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] +type CustomUserDataType = { + foo: string + bar: { + baz: number + } + en: 'ONE' | 'TWO' | 'THREE' +} + export type Database = { personal: { Tables: { users: { Row: { age_range: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } Insert: { age_range?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username: string } Update: { age_range?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username?: string } From daa87054605b799e3b087c285bc19c88b426a5df Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 01:17:21 +0900 Subject: [PATCH 2/5] feat(types): allow jsonpath selector types overrides --- src/select-query-parser/result.ts | 30 ++++++++++++++++++++-- src/select-query-parser/utils.ts | 30 +++++++++++++++++++--- test/index.test-d.ts | 30 ++++++++++++++++++++-- test/select-query-parser/parser.test-d.ts | 25 ++++++++++++++++++ test/select-query-parser/result.test-d.ts | 31 +++++++++++++++++++++++ 5 files changed, 139 insertions(+), 7 deletions(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 4af7d05e..c1cd42ef 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -15,6 +15,8 @@ import { GetFieldNodeResultName, IsAny, IsRelationNullable, + IsStringUnion, + JsonPathToType, ResolveRelationship, SelectQueryError, } from './utils' @@ -239,6 +241,30 @@ type ProcessFieldNode< ? ProcessEmbeddedResource : ProcessSimpleField +type ResolveJsonPathType< + Value, + Path extends string | undefined, + CastType extends PostgreSQLTypes +> = Path extends string + ? JsonPathToType extends never + ? // Always fallback if JsonPathToType returns never + TypeScriptTypes + : JsonPathToType extends infer PathResult + ? PathResult extends string + ? // Use the result if it's a string as we know that even with the string accessor ->> it's a valid type + PathResult + : IsStringUnion extends true + ? // Use the result if it's a union of strings + PathResult + : CastType extends 'json' + ? // If the type is not a string, ensure it was accessed with json accessor -> + PathResult + : // Otherwise it means non-string value accessed with string accessor ->> use the TypeScriptTypes result + TypeScriptTypes + : TypeScriptTypes + : // No json path, use regular type casting + TypeScriptTypes + /** * Processes a simple field (without embedded resources). * @@ -261,8 +287,8 @@ type ProcessSimpleField< } : { // Aliases override the property name in the result - [K in GetFieldNodeResultName]: Field['castType'] extends PostgreSQLTypes // We apply the detected casted as the result type - ? TypeScriptTypes + [K in GetFieldNodeResultName]: Field['castType'] extends PostgreSQLTypes + ? ResolveJsonPathType : Row[Field['name']] } : SelectQueryError<`column '${Field['name']}' does not exist on '${RelationName}'.`> diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 14be31d4..dac15f08 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -546,9 +546,33 @@ export type FindFieldMatchingRelationships< : SelectQueryError<'Failed to find matching relation via name'> export type JsonPathToAccessor = Path extends `${infer P1}->${infer P2}` - ? P2 extends `>${infer Rest}` // Check if P2 starts with > (from ->>) + ? P2 extends `>${infer Rest}` // Handle ->> operator ? JsonPathToAccessor<`${P1}.${Rest}`> - : JsonPathToAccessor<`${P1}.${P2}`> + : P2 extends string // Handle -> operator + ? JsonPathToAccessor<`${P1}.${P2}`> + : Path + : Path extends `>${infer Rest}` // Clean up any remaining > characters + ? JsonPathToAccessor : Path extends `${infer P1}::${infer _}` // Handle type casting - ? JsonPathToAccessor // Process the path without the cast type + ? JsonPathToAccessor : Path + +export type JsonPathToType = Path extends '' + ? T + : ContainsNull extends true + ? JsonPathToType, Path> + : Path extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? JsonPathToType + : never + : Path extends keyof T + ? T[Path] + : never + +export type IsStringUnion = string extends T + ? false + : T extends string + ? [T] extends [never] + ? false + : true + : false diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 3e318615..93414db0 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -214,11 +214,37 @@ const postgrest = new PostgrestClient(REST_URL) // Json Accessor with custom types overrides { - const { error } = await postgrest + const { data, error } = await postgrest .schema('personal') .from('users') - .select('data->foo->bar, data->foo->>baz') + .select('data->bar->baz, data->en, data->bar') + if (error) { + throw new Error(error.message) + } + expectType< + { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + }[] + >(data) +} +// Json string Accessor with custom types overrides +{ + const { data, error } = await postgrest + .schema('personal') + .from('users') + .select('data->bar->>baz, data->>en, data->>bar') if (error) { throw new Error(error.message) } + expectType< + { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + }[] + >(data) } diff --git a/test/select-query-parser/parser.test-d.ts b/test/select-query-parser/parser.test-d.ts index 51789a99..759f9724 100644 --- a/test/select-query-parser/parser.test-d.ts +++ b/test/select-query-parser/parser.test-d.ts @@ -103,6 +103,31 @@ import { selectParams } from '../relationships' }, ]) } +{ + expectTypepreferences->>theme, data->>some, data->foo->bar->>biz'>>([ + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'text', + jsonPath: 'preferences.theme', + }, + { + type: 'field', + name: 'data', + alias: 'some', + castType: 'text', + jsonPath: 'some', + }, + { + type: 'field', + name: 'data', + alias: 'biz', + castType: 'text', + jsonPath: 'foo.bar.biz', + }, + ]) +} // Select with spread { expectType>([ diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 74a40836..89615ba7 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -118,3 +118,34 @@ type SelectQueryFromTableResult< expectType(result2!) expectType(result3!) } + +{ + type SelectQueryFromTableResult< + TableName extends keyof Database['personal']['Tables'], + Q extends string + > = GetResult< + Database['personal'], + Database['personal']['Tables'][TableName]['Row'], + TableName, + Database['personal']['Tables'][TableName]['Relationships'], + Q + > + + let result: SelectQueryFromTableResult<'users', `data->bar->baz, data->en, data->bar`> + let expected: { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + } + expectType>(true) + + let result2: SelectQueryFromTableResult<'users', `data->bar->>baz, data->>en, data->>bar`> + let expected2: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + expectType>(true) +} From a27eb9e7a0d13abf0da20ce5d0ee98cb41a68b7a Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 13:32:31 +0900 Subject: [PATCH 3/5] chore: add test for fallback --- test/select-query-parser/result.test-d.ts | 46 +++++++++++++++-------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 89615ba7..0e4c541a 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -120,7 +120,7 @@ type SelectQueryFromTableResult< } { - type SelectQueryFromTableResult< + type SelectQueryFromPersonalTableResult< TableName extends keyof Database['personal']['Tables'], Q extends string > = GetResult< @@ -130,22 +130,38 @@ type SelectQueryFromTableResult< Database['personal']['Tables'][TableName]['Relationships'], Q > - - let result: SelectQueryFromTableResult<'users', `data->bar->baz, data->en, data->bar`> - let expected: { - baz: number - en: 'ONE' | 'TWO' | 'THREE' - bar: { + // Should work with Json object accessor + { + let result: SelectQueryFromPersonalTableResult<'users', `data->bar->baz, data->en, data->bar`> + let expected: { baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } } + expectType>(true) } - expectType>(true) - - let result2: SelectQueryFromTableResult<'users', `data->bar->>baz, data->>en, data->>bar`> - let expected2: { - baz: string - en: 'ONE' | 'TWO' | 'THREE' - bar: string + // Should work with Json string accessor + { + let result: SelectQueryFromPersonalTableResult< + 'users', + `data->bar->>baz, data->>en, data->>bar` + > + let expected: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + expectType>(true) + } + // Should fallback to defaults if unknown properties are mentionned + { + let result: SelectQueryFromPersonalTableResult<'users', `data->bar->>nope, data->neither`> + let expected: { + nope: string + neither: Json + } + expectType>(true) } - expectType>(true) } From d30623264b8365bf89bc25d14df9aa7e82580c4e Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 13:40:43 +0900 Subject: [PATCH 4/5] chore: fix tests null result case --- test/index.test-d.ts | 134 +++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 93414db0..db1c8969 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -52,87 +52,93 @@ const postgrest = new PostgrestClient(REST_URL) ) { - const { data, error } = await postgrest.from('users').select('status').eq('status', 'ONLINE') - if (error) { - throw new Error(error.message) + const result = await postgrest.from('users').select('status').eq('status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) } - expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) } { - const { data, error } = await postgrest.from('users').select('status').neq('status', 'ONLINE') - if (error) { - throw new Error(error.message) + const result = await postgrest.from('users').select('status').neq('status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) } - expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) } { - const { data, error } = await postgrest + const result = await postgrest .from('users') .select('status') .in('status', ['ONLINE', 'OFFLINE']) - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) } { - const { data, error } = await postgrest + const result = await postgrest .from('best_friends') .select('users!first_user(status)') .eq('users.status', 'ONLINE') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) } { - const { data, error } = await postgrest + const result = await postgrest .from('best_friends') .select('users!first_user(status)') .neq('users.status', 'ONLINE') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) } { - const { data, error } = await postgrest + const result = await postgrest .from('best_friends') .select('users!first_user(status)') .in('users.status', ['ONLINE', 'OFFLINE']) - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) } } // can override result type { - const { data, error } = await postgrest + const result = await postgrest .from('users') .select('*, messages(*)') .returns<{ messages: { foo: 'bar' }[] }[]>() - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ foo: 'bar' }[]>(data[0].messages) + expectType<{ foo: 'bar' }[]>(result.data[0].messages) } { - const { data, error } = await postgrest + const result = await postgrest .from('users') .insert({ username: 'foo' }) .select('*, messages(*)') .returns<{ messages: { foo: 'bar' }[] }[]>() - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ foo: 'bar' }[]>(data[0].messages) + expectType<{ foo: 'bar' }[]>(result.data[0].messages) } // cannot update non-updatable views @@ -147,60 +153,54 @@ const postgrest = new PostgrestClient(REST_URL) // spread resource with single column in select query { - const { data, error } = await postgrest - .from('messages') - .select('message, ...users(status)') - .single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, ...users(status)').single() + if (result.error) { + throw new Error(result.error.message) } expectType<{ message: string | null; status: Database['public']['Enums']['user_status'] | null }>( - data + result.data ) } // spread resource with all columns in select query { - const { data, error } = await postgrest.from('messages').select('message, ...users(*)').single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, ...users(*)').single() + if (result.error) { + throw new Error(result.error.message) } expectType>( - data + result.data ) } // `count` in embedded resource { - const { data, error } = await postgrest.from('messages').select('message, users(count)').single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, users(count)').single() + if (result.error) { + throw new Error(result.error.message) } - expectType<{ message: string | null; users: { count: number } }>(data) + expectType<{ message: string | null; users: { count: number } }>(result.data) } // json accessor in select query { - const { data, error } = await postgrest - .from('users') - .select('data->foo->bar, data->foo->>baz') - .single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('users').select('data->foo->bar, data->foo->>baz').single() + if (result.error) { + throw new Error(result.error.message) } // getting this w/o the cast, not sure why: // Parameter type Json is declared too wide for argument type Json - expectType(data.bar) - expectType(data.baz) + expectType(result.data.bar) + expectType(result.data.baz) } // rpc return type { - const { data, error } = await postgrest.rpc('get_status') - if (error) { - throw new Error(error.message) + const result = await postgrest.rpc('get_status') + if (result.error) { + throw new Error(result.error.message) } - expectType<'ONLINE' | 'OFFLINE'>(data) + expectType<'ONLINE' | 'OFFLINE'>(result.data) } // PostgrestBuilder's children retains class when using inherited methods @@ -214,12 +214,12 @@ const postgrest = new PostgrestClient(REST_URL) // Json Accessor with custom types overrides { - const { data, error } = await postgrest + const result = await postgrest .schema('personal') .from('users') .select('data->bar->baz, data->en, data->bar') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } expectType< { @@ -229,16 +229,16 @@ const postgrest = new PostgrestClient(REST_URL) baz: number } }[] - >(data) + >(result.data) } // Json string Accessor with custom types overrides { - const { data, error } = await postgrest + const result = await postgrest .schema('personal') .from('users') .select('data->bar->>baz, data->>en, data->>bar') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } expectType< { @@ -246,5 +246,5 @@ const postgrest = new PostgrestClient(REST_URL) en: 'ONE' | 'TWO' | 'THREE' bar: string }[] - >(data) + >(result.data) } From a9842b27240948a1dbe3fb602af597f13584a951 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 11:25:27 +0900 Subject: [PATCH 5/5] fix: jsonpath with embeded tables --- src/select-query-parser/utils.ts | 2 ++ test/basic.ts | 13 ++++++--- test/select-query-parser/parser.test-d.ts | 33 +++++++++++++++++++++++ test/select-query-parser/result.test-d.ts | 29 ++++++++++++++++++++ test/select-query-parser/select.test-d.ts | 4 +-- test/types.ts | 8 +++--- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index dac15f08..bcbfef04 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -555,6 +555,8 @@ export type JsonPathToAccessor = Path extends `${infer P1}- ? JsonPathToAccessor : Path extends `${infer P1}::${infer _}` // Handle type casting ? JsonPathToAccessor + : Path extends `${infer P1}${')' | ','}${infer _}` // Handle closing parenthesis and comma + ? P1 : Path export type JsonPathToType = Path extends '' diff --git a/test/basic.ts b/test/basic.ts index 44daf354..60f9383b 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -1,5 +1,5 @@ import { PostgrestClient } from '../src/index' -import { Database } from './types' +import { CustomUserDataType, Database } from './types' const REST_URL = 'http://localhost:3000' const postgrest = new PostgrestClient(REST_URL) @@ -1615,7 +1615,10 @@ test('select with no match', async () => { }) test('update with no match - return=minimal', async () => { - const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing') + const res = await postgrest + .from('users') + .update({ data: '' as unknown as CustomUserDataType }) + .eq('username', 'missing') expect(res).toMatchInlineSnapshot(` Object { "count": null, @@ -1628,7 +1631,11 @@ test('update with no match - return=minimal', async () => { }) test('update with no match - return=representation', async () => { - const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing').select() + const res = await postgrest + .from('users') + .update({ data: '' as unknown as CustomUserDataType }) + .eq('username', 'missing') + .select() expect(res).toMatchInlineSnapshot(` Object { "count": null, diff --git a/test/select-query-parser/parser.test-d.ts b/test/select-query-parser/parser.test-d.ts index 759f9724..b241e13d 100644 --- a/test/select-query-parser/parser.test-d.ts +++ b/test/select-query-parser/parser.test-d.ts @@ -662,3 +662,36 @@ import { selectParams } from '../relationships' 0 as any as ParserError<'Unexpected input: ->->theme'> ) } + +// JSON accessor within embedded tables +{ + expectTypebar->>baz, data->>en, data->bar)'>>([ + { + type: 'field', + name: 'users', + children: [ + { + type: 'field', + name: 'data', + alias: 'baz', + castType: 'text', + jsonPath: 'bar.baz', + }, + { + type: 'field', + name: 'data', + alias: 'en', + castType: 'text', + jsonPath: 'en', + }, + { + type: 'field', + name: 'data', + alias: 'bar', + castType: 'json', + jsonPath: 'bar', + }, + ], + }, + ]) +} diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 0e4c541a..508424f0 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -164,4 +164,33 @@ type SelectQueryFromTableResult< } expectType>(true) } + // Should work with embeded Json object accessor + { + let result: SelectQueryFromTableResult<'messages', `users(data->bar->baz, data->en, data->bar)`> + let expected: { + users: { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + } + } + expectType>(true) + } + // Should work with embeded Json string accessor + { + let result: SelectQueryFromTableResult< + 'messages', + `users(data->bar->>baz, data->>en, data->>bar)` + > + let expected: { + users: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + } + expectType>(true) + } } diff --git a/test/select-query-parser/select.test-d.ts b/test/select-query-parser/select.test-d.ts index 2343fa6f..adc54416 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -3,7 +3,7 @@ import { TypeEqual } from 'ts-expect' import { Json } from '../../src/select-query-parser/types' import { SelectQueryError } from '../../src/select-query-parser/utils' import { Prettify } from '../../src/types' -import { Database } from '../types' +import { CustomUserDataType, Database } from '../types' import { selectQueries } from '../relationships' // This test file is here to ensure that for a query against a specfic datatabase @@ -617,7 +617,7 @@ type Schema = Database['public'] users: { age_range: unknown | null catchphrase: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } diff --git a/test/types.ts b/test/types.ts index 7a2954ef..ba4366e3 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,6 +1,6 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] -type CustomUserDataType = { +export type CustomUserDataType = { foo: string bar: { baz: number @@ -412,21 +412,21 @@ export type Database = { Row: { age_range: unknown | null catchphrase: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } Insert: { age_range?: unknown | null catchphrase?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username: string } Update: { age_range?: unknown | null catchphrase?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username?: string }