From 12faa3df5a400f3ff8d3be1ffce13dc11b2aeaf2 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 14 Jan 2025 19:01:27 +0900 Subject: [PATCH] fix(types): type result for throwOnError responses (#590) * fix(types): type result for throwOnError responses When using throwOnError(), the response type is now more strictly typed: - Data is guaranteed to be non-null - Error field is removed from response type - Response type is controlled by generic ThrowOnError boolean parameter Fixes #563 * chore: re-use generic types * fix: return this to comply with PostgresFilterBuilder * chore: fix test to check inheritance and not equality --- src/PostgrestBuilder.ts | 26 ++++++++---- src/PostgrestTransformBuilder.ts | 12 +++--- test/index.test-d.ts | 71 ++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 621785c9..2c3863e2 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -1,11 +1,14 @@ // @ts-ignore import nodeFetch from '@supabase/node-fetch' -import type { Fetch, PostgrestSingleResponse } from './types' +import type { Fetch, PostgrestSingleResponse, PostgrestResponseSuccess } from './types' import PostgrestError from './PostgrestError' -export default abstract class PostgrestBuilder - implements PromiseLike> +export default abstract class PostgrestBuilder + implements + PromiseLike< + ThrowOnError extends true ? PostgrestResponseSuccess : PostgrestSingleResponse + > { protected method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE' protected url: URL @@ -42,9 +45,9 @@ export default abstract class PostgrestBuilder * * {@link https://github.com/supabase/supabase-js/issues/92} */ - throwOnError(): this { + throwOnError(): this & PostgrestBuilder { this.shouldThrowOnError = true - return this + return this as this & PostgrestBuilder } /** @@ -56,9 +59,18 @@ export default abstract class PostgrestBuilder return this } - then, TResult2 = never>( + then< + TResult1 = ThrowOnError extends true + ? PostgrestResponseSuccess + : PostgrestSingleResponse, + TResult2 = never + >( onfulfilled?: - | ((value: PostgrestSingleResponse) => TResult1 | PromiseLike) + | (( + value: ThrowOnError extends true + ? PostgrestResponseSuccess + : PostgrestSingleResponse + ) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index 4791702c..2be085c8 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -192,7 +192,7 @@ export default class PostgrestTransformBuilder< ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never >(): PostgrestBuilder { this.headers['Accept'] = 'application/vnd.pgrst.object+json' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -212,7 +212,7 @@ export default class PostgrestTransformBuilder< this.headers['Accept'] = 'application/vnd.pgrst.object+json' } this.isMaybeSingle = true - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -220,7 +220,7 @@ export default class PostgrestTransformBuilder< */ csv(): PostgrestBuilder { this.headers['Accept'] = 'text/csv' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -228,7 +228,7 @@ export default class PostgrestTransformBuilder< */ geojson(): PostgrestBuilder> { this.headers['Accept'] = 'application/geo+json' - return this as PostgrestBuilder> + return this as unknown as PostgrestBuilder> } /** @@ -285,8 +285,8 @@ export default class PostgrestTransformBuilder< this.headers[ 'Accept' ] = `application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};` - if (format === 'json') return this as PostgrestBuilder[]> - else return this as PostgrestBuilder + if (format === 'json') return this as unknown as PostgrestBuilder[]> + else return this as unknown as PostgrestBuilder } /** diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 50a2444d..745b8c40 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -1,5 +1,6 @@ +import { TypeEqual } from 'ts-expect' import { expectError, expectType } from 'tsd' -import { PostgrestClient } from '../src/index' +import { PostgrestClient, PostgrestError } from '../src/index' import { Prettify } from '../src/types' import { Database, Json } from './types' @@ -208,6 +209,70 @@ const postgrest = new PostgrestClient(REST_URL) const x = postgrest.from('channels').select() const y = x.throwOnError() const z = x.setHeader('', '') - expectType(y) - expectType(z) + expectType(true) + expectType(true) +} + +// Should have nullable data and error field +{ + const result = await postgrest.from('users').select('username, messages(id, message)').limit(1) + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + | null + const { data } = result + const { error } = result + expectType>(true) + let err: PostgrestError | null + expectType>(true) +} + +// Should have non nullable data and no error fields if throwOnError is added +{ + const result = await postgrest + .from('users') + .select('username, messages(id, message)') + .limit(1) + .throwOnError() + const { data } = result + const { error } = result + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + expectType>(true) + expectType>(true) + error +} + +// Should work with throwOnError middle of the chaining +{ + const result = await postgrest + .from('users') + .select('username, messages(id, message)') + .throwOnError() + .eq('username', 'test') + .limit(1) + const { data } = result + const { error } = result + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + expectType>(true) + expectType>(true) + error }