From 4fef0589cead6590fb97054a1f28c84d87e86bc1 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 17 Feb 2025 20:18:46 +0900 Subject: [PATCH 01/10] fix(types): returns type casting - Allow to use returns at the end of the call chain after a .single() - Add deprecation warning to move toward explicit overrideTypes method - Add cast checking basic logic, an array should be declared if the result is an array, an object if the result is an object --- src/PostgrestBuilder.ts | 20 +++++- src/PostgrestTransformBuilder.ts | 7 +- src/types.ts | 22 ++++++ test/returns.test-d.ts | 117 +++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 test/returns.test-d.ts diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 2c3863e2..36683a6d 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -1,7 +1,12 @@ // @ts-ignore import nodeFetch from '@supabase/node-fetch' -import type { Fetch, PostgrestSingleResponse, PostgrestResponseSuccess } from './types' +import type { + Fetch, + PostgrestSingleResponse, + PostgrestResponseSuccess, + CheckMatchingArrayTypes, +} from './types' import PostgrestError from './PostgrestError' export default abstract class PostgrestBuilder @@ -209,4 +214,17 @@ export default abstract class PostgrestBuilder() method at the end of your call chain instead + */ + returns(): PostgrestBuilder, ThrowOnError> { + return this as unknown as PostgrestBuilder< + CheckMatchingArrayTypes, + ThrowOnError + > + } } diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index 2be085c8..5ce64df7 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -1,6 +1,6 @@ import PostgrestBuilder from './PostgrestBuilder' import { GetResult } from './select-query-parser/result' -import { GenericSchema } from './types' +import { GenericSchema, CheckMatchingArrayTypes } from './types' export default class PostgrestTransformBuilder< Schema extends GenericSchema, @@ -307,18 +307,19 @@ export default class PostgrestTransformBuilder< * Override the type of the returned `data`. * * @typeParam NewResult - The new result type to override with + * @deprecated Use overrideTypes() method at the end of your call chain instead */ returns(): PostgrestTransformBuilder< Schema, Row, - NewResult, + CheckMatchingArrayTypes, RelationName, Relationships > { return this as unknown as PostgrestTransformBuilder< Schema, Row, - NewResult, + CheckMatchingArrayTypes, RelationName, Relationships > diff --git a/src/types.ts b/src/types.ts index fd1378ce..dbe04387 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import PostgrestError from './PostgrestError' +import { SelectQueryError } from './select-query-parser/utils' export type Fetch = typeof fetch @@ -89,3 +90,24 @@ type ConditionalSimplifyDeep< type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown) type BuiltIns = Primitive | void | Date | RegExp type Primitive = null | undefined | string | number | boolean | symbol | bigint + +/** + * Utility type to check if array types match between Result and NewResult. + * Returns either the valid NewResult type or an error message type. + */ +export type CheckMatchingArrayTypes = + // If the result is a QueryError we allow the user to override anyway + Result extends SelectQueryError + ? NewResult + : // Otherwise, we check basic type matching (array should be override by array, object by object) + Result extends any[] + ? NewResult extends any[] + ? NewResult // Both are arrays - valid + : { + Error: 'Type mismatch: Cannot cast array result to a single object. Use .returns> for array results or .single() to convert the result to a single object' + } + : NewResult extends any[] + ? { + Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' + } + : NewResult // Neither are arrays - valid diff --git a/test/returns.test-d.ts b/test/returns.test-d.ts new file mode 100644 index 00000000..0f27c225 --- /dev/null +++ b/test/returns.test-d.ts @@ -0,0 +1,117 @@ +import { expectType } from 'tsd' +import { PostgrestBuilder, PostgrestClient } from '../src/index' +import { Database } from './types' +import { TypeEqual } from 'ts-expect' + +const REST_URL = 'http://localhost:3000' +const postgrest = new PostgrestClient(REST_URL) + +// Test returns() with different end methods +{ + // Test with single() + const singleResult = await postgrest + .from('users') + .select() + .single() + .returns<{ username: string }>() + if (singleResult.error) { + throw new Error(singleResult.error.message) + } + let result: typeof singleResult.data + let expected: { username: string } + expectType>(true) + + // Test with maybeSingle() + const maybeSingleResult = await postgrest + .from('users') + .select() + .maybeSingle() + .returns<{ username: string }>() + if (maybeSingleResult.error) { + throw new Error(maybeSingleResult.error.message) + } + let maybeSingleResultType: typeof maybeSingleResult.data + let maybeSingleExpected: { username: string } + expectType>(true) + + // Test array to non-array type casting error + const invalidCastArray = (await postgrest.from('users').select().returns<{ username: string }>()) + .data + expectType({ + Error: + 'Type mismatch: Cannot cast array result to a single object. Use .returns> for array results or .single() to convert the result to a single object', + }) + + // Test non-array to array type casting error + const invalidCastSingle = postgrest + .from('users') + .select() + .single() + .returns<{ username: string }[]>() + expectType< + PostgrestBuilder< + { + Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' + }, + false + > + >(invalidCastSingle) + + // Test with csv() + const csvResult = await postgrest.from('users').select().csv().returns() + if (csvResult.error) { + throw new Error(csvResult.error.message) + } + let csvResultType: typeof csvResult.data + let csvExpected: string + expectType>(true) + + // Test with throwOnError() + const throwResult = await postgrest + .from('users') + .select() + .returns<{ username: string }[]>() + .throwOnError() + let throwResultType: typeof throwResult.data + let throwExpected: { username: string }[] + expectType>(true) + let throwErrorType: typeof throwResult.error + let throwErrorExpected: null + expectType>(true) +} + +// Test returns() with nested selects and relationships +{ + const result = await postgrest + .from('users') + .select('username, messages(id, content)') + .single() + .returns<{ + username: string + messages: { id: number; content: string }[] + }>() + if (result.error) { + throw new Error(result.error.message) + } + let resultType: typeof result.data + let expected: { + username: string + messages: { id: number; content: string }[] + } + expectType>(true) +} + +// Test returns() with JSON operations +{ + const result = await postgrest + .from('users') + .select('data->settings') + .single() + .returns<{ settings: { theme: 'light' | 'dark' } }>() + if (result.error) { + throw new Error(result.error.message) + } + let resultType: typeof result.data + let expected: { settings: { theme: 'light' | 'dark' } } + expectType>(true) +} From bf0650bbc8b9df620244dc0f82042867bbc4df7e Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 17 Feb 2025 21:33:26 +0900 Subject: [PATCH 02/10] chore: fix tests --- test/returns.test-d.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/returns.test-d.ts b/test/returns.test-d.ts index 0f27c225..e508af88 100644 --- a/test/returns.test-d.ts +++ b/test/returns.test-d.ts @@ -35,12 +35,15 @@ const postgrest = new PostgrestClient(REST_URL) expectType>(true) // Test array to non-array type casting error - const invalidCastArray = (await postgrest.from('users').select().returns<{ username: string }>()) - .data - expectType({ - Error: - 'Type mismatch: Cannot cast array result to a single object. Use .returns> for array results or .single() to convert the result to a single object', - }) + const invalidCastArray = await postgrest.from('users').select().returns<{ username: string }>() + if (invalidCastArray.error) { + throw new Error(invalidCastArray.error.message) + } + let resultType: typeof invalidCastArray.data + let resultExpected: { + Error: 'Type mismatch: Cannot cast array result to a single object. Use .returns> for array results or .single() to convert the result to a single object' + } + expectType>(true) // Test non-array to array type casting error const invalidCastSingle = postgrest From 950a6fffe98735538414e6bf93c369323c7c2d14 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 19 Feb 2025 10:46:24 +0900 Subject: [PATCH 03/10] fix: preserve result optionality in override --- src/PostgrestBuilder.ts | 2 +- src/types.ts | 7 ++++++- test/returns.test-d.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 36683a6d..6dd7112c 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -219,7 +219,7 @@ export default abstract class PostgrestBuilder() method at the end of your call chain instead + * @deprecated Use overrideTypes() method at the end of your call chain instead */ returns(): PostgrestBuilder, ThrowOnError> { return this as unknown as PostgrestBuilder< diff --git a/src/types.ts b/src/types.ts index dbe04387..6354a8b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import PostgrestError from './PostgrestError' +import { ContainsNull } from './select-query-parser/types' import { SelectQueryError } from './select-query-parser/utils' export type Fetch = typeof fetch @@ -110,4 +111,8 @@ export type CheckMatchingArrayTypes = ? { Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' } - : NewResult // Neither are arrays - valid + : // Neither are arrays - valid + // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) + ContainsNull extends true + ? NewResult | null + : NewResult diff --git a/test/returns.test-d.ts b/test/returns.test-d.ts index e508af88..551a0e22 100644 --- a/test/returns.test-d.ts +++ b/test/returns.test-d.ts @@ -31,7 +31,7 @@ const postgrest = new PostgrestClient(REST_URL) throw new Error(maybeSingleResult.error.message) } let maybeSingleResultType: typeof maybeSingleResult.data - let maybeSingleExpected: { username: string } + let maybeSingleExpected: { username: string } | null expectType>(true) // Test array to non-array type casting error From a479b87d7ee9f545d78d1d5a1fe456465a7e9c8d Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 19 Feb 2025 10:46:42 +0900 Subject: [PATCH 04/10] fix: add runtime test coverage --- test/basic.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/basic.ts b/test/basic.ts index 37b8879e..0d6f5589 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -116,6 +116,30 @@ test('basic select returns types override', async () => { `) }) +test('basic select returns from builder', async () => { + const res = await postgrest + .from('users') + .select() + .eq('username', 'supabot') + .single() + .returns<{ status: 'ONLINE' | 'OFFLINE' }>() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('basic select view', async () => { const res = await postgrest.from('updatable_view').select() expect(res).toMatchInlineSnapshot(` From 963bc7eaea1738bd9ba31f6140ae15d6e428a196 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 17 Feb 2025 21:13:54 +0900 Subject: [PATCH 05/10] feat(types): add overrideTypes method Allow both partial and complete override of the return type, do partial override by default --- src/PostgrestBuilder.ts | 41 +++++++++++ src/types.ts | 66 ++++++++++++++---- test/basic.ts | 24 +++++++ test/override-types.test-d.ts | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 test/override-types.test-d.ts diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 6dd7112c..fcb7bf65 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -6,6 +6,8 @@ import type { PostgrestSingleResponse, PostgrestResponseSuccess, CheckMatchingArrayTypes, + MergePartialResult, + IsValidResultOverride, } from './types' import PostgrestError from './PostgrestError' @@ -227,4 +229,43 @@ export default abstract class PostgrestBuilder } + + /** + * Override the type of the returned `data` field in the response. + * + * @typeParam NewResult - The new type to cast the response data to + * @typeParam Options - Optional type configuration (defaults to { merge: true }) + * @typeParam Options.merge - When true, merges the new type with existing return type. When false, replaces the existing types entirely (defaults to true) + * @example + * ```typescript + * // Merge with existing types (default behavior) + * const query = supabase + * .from('users') + * .select() + * .overrideTypes<{ custom_field: string }>() + * + * // Replace existing types completely + * const replaceQuery = supabase + * .from('users') + * .select() + * .overrideTypes<{ id: number; name: string }, { merge: false }>() + * ``` + * @returns A PostgrestBuilder instance with the new type + */ + overrideTypes< + NewResult, + Options extends { merge?: boolean } = { merge: true } + >(): PostgrestBuilder< + IsValidResultOverride extends true + ? MergePartialResult + : CheckMatchingArrayTypes, + ThrowOnError + > { + return this as unknown as PostgrestBuilder< + IsValidResultOverride extends true + ? MergePartialResult + : CheckMatchingArrayTypes, + ThrowOnError + > + } } diff --git a/src/types.ts b/src/types.ts index 6354a8b6..9f4da6f7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -92,6 +92,20 @@ type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unk type BuiltIns = Primitive | void | Date | RegExp type Primitive = null | undefined | string | number | boolean | symbol | bigint +export type IsValidResultOverride = + Result extends any[] + ? NewResult extends any[] + ? // Both are arrays - valid + Ok + : ErrorResult + : NewResult extends any[] + ? ErrorNewResult + : // Neither are arrays - valid + // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) + ContainsNull extends true + ? Ok | null + : Ok + /** * Utility type to check if array types match between Result and NewResult. * Returns either the valid NewResult type or an error message type. @@ -100,19 +114,43 @@ export type CheckMatchingArrayTypes = // If the result is a QueryError we allow the user to override anyway Result extends SelectQueryError ? NewResult - : // Otherwise, we check basic type matching (array should be override by array, object by object) - Result extends any[] - ? NewResult extends any[] - ? NewResult // Both are arrays - valid - : { + : IsValidResultOverride< + Result, + NewResult, + NewResult, + { Error: 'Type mismatch: Cannot cast array result to a single object. Use .returns> for array results or .single() to convert the result to a single object' + }, + { + Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' } - : NewResult extends any[] - ? { - Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' - } - : // Neither are arrays - valid - // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`) - ContainsNull extends true - ? NewResult | null - : NewResult + > + +type Simplify = T extends object ? { [K in keyof T]: T[K] } : T + +type MergeDeep = { + [K in keyof New | keyof Row]: K extends keyof Row + ? K extends keyof New + ? IsPlainObject extends true + ? IsPlainObject extends true + ? MergeDeep + : Row[K] + : Row[K] + : Row[K] + : K extends keyof New + ? New[K] + : never +} + +// Helper to check if a type is a plain object (not an array) +type IsPlainObject = T extends any[] ? false : T extends object ? true : false + +// Merge the new result with the original (Result) when partial is true. +// If NewResult is an array, merge each element. +export type MergePartialResult = Options extends { merge: true } + ? Result extends any[] + ? NewResult extends any[] + ? Array>> + : never + : Simplify> + : NewResult diff --git a/test/basic.ts b/test/basic.ts index 0d6f5589..1bad8447 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -140,6 +140,30 @@ test('basic select returns from builder', async () => { `) }) +test('basic select overrideTypes from builder', async () => { + const res = await postgrest + .from('users') + .select() + .eq('username', 'supabot') + .single() + .overrideTypes<{ status: 'ONLINE' | 'OFFLINE' }>() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('basic select view', async () => { const res = await postgrest.from('updatable_view').select() expect(res).toMatchInlineSnapshot(` diff --git a/test/override-types.test-d.ts b/test/override-types.test-d.ts new file mode 100644 index 00000000..e22aeadf --- /dev/null +++ b/test/override-types.test-d.ts @@ -0,0 +1,126 @@ +import { expectType } from 'tsd' +import { TypeEqual } from 'ts-expect' +import { PostgrestClient } from '../src' +import { CustomUserDataType, Database } from './types' + +const REST_URL = 'http://localhost:54321' +const postgrest = new PostgrestClient(REST_URL) + +// Test merge array result to object should error +{ + const singleResult = await postgrest + .from('users') + .select() + .overrideTypes<{ custom_field: string }>() + if (singleResult.error) { + throw new Error(singleResult.error.message) + } + let result: typeof singleResult.data + expectType< + TypeEqual< + typeof result, + { + Error: 'Type mismatch: Cannot cast array result to a single object. Use .returns> for array results or .single() to convert the result to a single object' + } + > + >(true) +} + +// Test merge object result to array type should error +{ + const singleResult = await postgrest + .from('users') + .select() + .single() + .overrideTypes<{ custom_field: string }[]>() + if (singleResult.error) { + throw new Error(singleResult.error.message) + } + let result: typeof singleResult.data + expectType< + TypeEqual< + typeof result, + { + Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain' + } + > + >(true) +} + +// Test with single() / maybeSingle() +{ + const singleResult = await postgrest + .from('users') + .select() + .single() + .overrideTypes<{ custom_field: string }>() + if (singleResult.error) { + throw new Error(singleResult.error.message) + } + let result: typeof singleResult.data + expectType>(true) +} +// Test with maybeSingle() +{ + const maybeSingleResult = await postgrest + .from('users') + .select() + .maybeSingle() + .overrideTypes<{ custom_field: string }>() + if (maybeSingleResult.error) { + throw new Error(maybeSingleResult.error.message) + } + let maybeSingleResultType: typeof maybeSingleResult.data + let expectedType: { custom_field: string } | null + expectType>(true) +} +// Test replacing behavior +{ + const singleResult = await postgrest + .from('users') + .select() + .single() + .overrideTypes<{ custom_field: string }, { merge: false }>() + if (singleResult.error) { + throw new Error(singleResult.error.message) + } + let result: typeof singleResult.data + expectType>(true) +} + +// Test with select() +{ + const singleResult = await postgrest + .from('users') + .select() + .overrideTypes<{ custom_field: string }[]>() + if (singleResult.error) { + throw new Error(singleResult.error.message) + } + let result: typeof singleResult.data + expectType< + TypeEqual< + typeof result, + { + username: string + data: CustomUserDataType | null + age_range: unknown + catchphrase: unknown + status: 'ONLINE' | 'OFFLINE' | null + custom_field: string + }[] + > + >(true) +} +// Test replacing select behavior +{ + const singleResult = await postgrest + .from('users') + .select() + .overrideTypes<{ custom_field: string }[], { merge: false }>() + if (singleResult.error) { + throw new Error(singleResult.error.message) + } + let result: typeof singleResult.data + expectType>(true) +} From 5f24f39a6b66d633fccab1bd83c8478625f2e583 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 17 Feb 2025 21:17:14 +0900 Subject: [PATCH 06/10] chore: fix typo --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 9f4da6f7..2341524e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -145,7 +145,7 @@ type MergeDeep = { // Helper to check if a type is a plain object (not an array) type IsPlainObject = T extends any[] ? false : T extends object ? true : false -// Merge the new result with the original (Result) when partial is true. +// Merge the new result with the original (Result) when merge option is true. // If NewResult is an array, merge each element. export type MergePartialResult = Options extends { merge: true } ? Result extends any[] From 6aaef170425b35ec19b23ca77896082fffba55d4 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 19 Feb 2025 17:21:54 +0900 Subject: [PATCH 07/10] chore(coverage): exclude tests code from coverage report --- jest.config.js | 1 + src/PostgrestBuilder.ts | 1 + test/basic.ts | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/jest.config.js b/jest.config.js index 87c30aa4..bc64026a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,5 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + collectCoverageFrom: ['src/**/*'], } diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 6dd7112c..19e3d497 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -222,6 +222,7 @@ export default abstract class PostgrestBuilder() method at the end of your call chain instead */ returns(): PostgrestBuilder, ThrowOnError> { + /* istanbul ignore next */ return this as unknown as PostgrestBuilder< CheckMatchingArrayTypes, ThrowOnError diff --git a/test/basic.ts b/test/basic.ts index 0d6f5589..90225521 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -140,6 +140,42 @@ test('basic select returns from builder', async () => { `) }) +test('basic select with maybeSingle yielding more than one result', async () => { + const res = await postgrest.from('users').select().maybeSingle() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST116", + "details": "Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row", + "hint": null, + "message": "JSON object requested, multiple (or no) rows returned", + }, + "status": 406, + "statusText": "Not Acceptable", + } + `) +}) + +test('basic select with single yielding more than one result', async () => { + const res = await postgrest.from('users').select().single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST116", + "details": "The result contains 5 rows", + "hint": null, + "message": "JSON object requested, multiple (or no) rows returned", + }, + "status": 406, + "statusText": "Not Acceptable", + } + `) +}) + test('basic select view', async () => { const res = await postgrest.from('updatable_view').select() expect(res).toMatchInlineSnapshot(` From 4c117fab8db3b1af0d5c31a551562ddb55fc500e Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 19 Feb 2025 17:22:37 +0900 Subject: [PATCH 08/10] chore: apply PR comment --- src/PostgrestTransformBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index 5ce64df7..c9fb5781 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -307,7 +307,7 @@ export default class PostgrestTransformBuilder< * Override the type of the returned `data`. * * @typeParam NewResult - The new result type to override with - * @deprecated Use overrideTypes() method at the end of your call chain instead + * @deprecated Use overrideTypes() method at the end of your call chain instead */ returns(): PostgrestTransformBuilder< Schema, From e84c452e3fc74a9b417801ad1568cb2710373e39 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 19 Feb 2025 17:40:09 +0900 Subject: [PATCH 09/10] chore: increase test coverage --- test/basic.ts | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/test/basic.ts b/test/basic.ts index 90225521..34423ee2 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -2023,3 +2023,187 @@ test('join on 1-1 relation with nullables', async () => { } `) }) + +test('custom fetch function', async () => { + const customFetch = jest.fn().mockImplementation(() => + Promise.resolve({ + ok: true, + status: 200, + statusText: 'OK', + text: () => Promise.resolve('[]'), + }) + ) + + const postgrestWithCustomFetch = new PostgrestClient(REST_URL, { + fetch: customFetch, + }) + + await postgrestWithCustomFetch.from('users').select() + + expect(customFetch).toHaveBeenCalledWith( + expect.stringContaining(REST_URL), + expect.objectContaining({ + method: 'GET', + headers: expect.any(Object), + }) + ) +}) + +test('handles undefined global fetch', async () => { + // Store original fetch + const originalFetch = globalThis.fetch + // Delete global fetch to simulate environments where it's undefined + delete (globalThis as any).fetch + + try { + const postgrestClient = new PostgrestClient(REST_URL) + const result = await postgrestClient.from('users').select() + expect(result).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'cat'", + "data": null, + "status": "OFFLINE", + "username": "kiwicopple", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "awailas", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'json' 'test'", + "data": Object { + "foo": Object { + "bar": Object { + "nested": "value", + }, + "baz": "string value", + }, + }, + "status": "ONLINE", + "username": "jsonuser", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) + // Test passes if we reach here without errors, as it means nodeFetch was used + } finally { + // Restore original fetch + globalThis.fetch = originalFetch + } +}) + +test('handles array error with 404 status', async () => { + // Mock the fetch response to return an array error with 404 + const customFetch = jest.fn().mockImplementation(() => + Promise.resolve({ + ok: false, + status: 404, + statusText: 'Not Found', + text: () => Promise.resolve('[]'), + }) + ) + + const postgrestWithCustomFetch = new PostgrestClient(REST_URL, { + fetch: customFetch, + }) + + const res = await postgrestWithCustomFetch.from('users').select() + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('handles empty body with 404 status', async () => { + // Mock the fetch response to return an empty body with 404 + const customFetch = jest.fn().mockImplementation(() => + Promise.resolve({ + ok: false, + status: 404, + statusText: 'Not Found', + text: () => Promise.resolve(''), + }) + ) + + const postgrestWithCustomFetch = new PostgrestClient(REST_URL, { + fetch: customFetch, + }) + + const res = await postgrestWithCustomFetch.from('users').select() + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 204, + "statusText": "No Content", + } + `) +}) + +test('maybeSingle handles zero rows error', async () => { + const customFetch = jest.fn().mockImplementation(() => + Promise.resolve({ + ok: false, + status: 406, + statusText: 'Not Acceptable', + text: () => + Promise.resolve( + JSON.stringify({ + code: 'PGRST116', + details: '0 rows', + hint: null, + message: 'JSON object requested, multiple (or no) rows returned', + }) + ), + }) + ) + + const postgrestWithCustomFetch = new PostgrestClient(REST_URL, { + fetch: customFetch, + }) + + const res = await postgrestWithCustomFetch.from('users').select().maybeSingle() + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) From 97f50085205e5130698400580dfcee713ac1e6d6 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 19 Feb 2025 17:48:40 +0900 Subject: [PATCH 10/10] chore: format --- test/basic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/basic.ts b/test/basic.ts index 2d107746..71d884fa 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -161,7 +161,7 @@ test('basic select overrideTypes from builder', async () => { "status": 200, "statusText": "OK", } - `); + `) }) test('basic select with maybeSingle yielding more than one result', async () => {