diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml new file mode 100644 index 00000000..092d0f95 --- /dev/null +++ b/.github/workflows/pkg.pr.new.yml @@ -0,0 +1,14 @@ +name: pkg.pr.new + +on: + push: {} + +jobs: + run: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - run: npm clean-install + - run: npm run build + - name: Create pkg.pr.new release + run: npx pkg-pr-new publish diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 621785c9..a52169c1 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -1,10 +1,10 @@ // @ts-ignore import nodeFetch from '@supabase/node-fetch' -import type { Fetch, PostgrestSingleResponse } from './types' +import type { Fetch, PostgrestResponseSuccess, PostgrestSingleResponse } from './types' import PostgrestError from './PostgrestError' -export default abstract class PostgrestBuilder +export default abstract class PostgrestBuilder implements PromiseLike> { protected method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE' @@ -12,12 +12,12 @@ export default abstract class PostgrestBuilder protected headers: Record protected schema?: string protected body?: unknown - protected shouldThrowOnError = false + protected shouldThrowOnError: boolean protected signal?: AbortSignal protected fetch: Fetch protected isMaybeSingle: boolean - constructor(builder: PostgrestBuilder) { + constructor(builder: PostgrestBuilder) { this.method = builder.method this.url = builder.url this.headers = builder.headers @@ -36,15 +36,9 @@ export default abstract class PostgrestBuilder } } - /** - * If there's an error with the query, throwOnError will reject the promise by - * throwing the error instead of returning it as part of a successful response. - * - * {@link https://github.com/supabase/supabase-js/issues/92} - */ - throwOnError(): this { + throwOnError(): PostgrestBuilder { this.shouldThrowOnError = true - return this + return this as PostgrestBuilder } /** @@ -58,7 +52,11 @@ export default abstract class PostgrestBuilder then, 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/PostgrestClient.ts b/src/PostgrestClient.ts index 915ddc16..a2196185 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -21,12 +21,14 @@ export default class PostgrestClient< : string & keyof Database, Schema extends GenericSchema = Database[SchemaName] extends GenericSchema ? Database[SchemaName] - : any + : any, + ThrowOnError extends boolean = false > { url: string headers: Record schemaName?: SchemaName fetch?: Fetch + shouldThrowOnError: ThrowOnError // TODO: Add back shouldThrowOnError once we figure out the typings /** @@ -44,36 +46,63 @@ export default class PostgrestClient< headers = {}, schema, fetch, + shouldThrowOnError, }: { headers?: Record schema?: SchemaName fetch?: Fetch + shouldThrowOnError?: ThrowOnError } = {} ) { this.url = url this.headers = { ...DEFAULT_HEADERS, ...headers } this.schemaName = schema this.fetch = fetch + this.shouldThrowOnError = Boolean(shouldThrowOnError) as ThrowOnError + } + + throwOnError(): PostgrestClient { + return new PostgrestClient(this.url, { + headers: this.headers, + schema: this.schemaName, + fetch: this.fetch, + shouldThrowOnError: true, + }) as PostgrestClient } from< TableName extends string & keyof Schema['Tables'], Table extends Schema['Tables'][TableName] - >(relation: TableName): PostgrestQueryBuilder + >( + relation: TableName + ): PostgrestQueryBuilder< + Schema, + Table, + any, + Table extends { Relationships: infer R } ? R : unknown, + ThrowOnError + > from( relation: ViewName - ): PostgrestQueryBuilder + ): PostgrestQueryBuilder< + Schema, + View, + any, + View extends { Relationships: infer R } ? R : unknown, + ThrowOnError + > /** * Perform a query on a table or a view. * * @param relation - The table or view name to query */ - from(relation: string): PostgrestQueryBuilder { + from(relation: string): PostgrestQueryBuilder { const url = new URL(`${this.url}/${relation}`) - return new PostgrestQueryBuilder(url, { + return new PostgrestQueryBuilder(url, { headers: { ...this.headers }, schema: this.schemaName, fetch: this.fetch, + shouldThrowOnError: this.shouldThrowOnError, }) } @@ -89,12 +118,14 @@ export default class PostgrestClient< ): PostgrestClient< Database, DynamicSchema, - Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any + Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any, + ThrowOnError > { return new PostgrestClient(this.url, { headers: this.headers, schema, fetch: this.fetch, + shouldThrowOnError: this.shouldThrowOnError, }) } @@ -140,7 +171,9 @@ export default class PostgrestClient< ? Fn['Returns'][number] : never : never, - Fn['Returns'] + Fn['Returns'], + unknown, + ThrowOnError > { let method: 'HEAD' | 'GET' | 'POST' const url = new URL(`${this.url}/rpc/${fn}`) @@ -174,6 +207,7 @@ export default class PostgrestClient< body, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + shouldThrowOnError: this.shouldThrowOnError, + } as never) } } diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 21cc4090..daf3399e 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -30,8 +30,16 @@ export default class PostgrestFilterBuilder< Row extends Record, Result, RelationName = unknown, - Relationships = unknown -> extends PostgrestTransformBuilder { + Relationships = unknown, + ThrowOnError extends boolean = false +> extends PostgrestTransformBuilder< + Schema, + Row, + Result, + RelationName, + Relationships, + ThrowOnError +> { eq( column: ColumnName, value: NonNullable diff --git a/src/PostgrestQueryBuilder.ts b/src/PostgrestQueryBuilder.ts index 8f6db3f5..a5ae966b 100644 --- a/src/PostgrestQueryBuilder.ts +++ b/src/PostgrestQueryBuilder.ts @@ -7,13 +7,15 @@ export default class PostgrestQueryBuilder< Schema extends GenericSchema, Relation extends GenericTable | GenericView, RelationName = unknown, - Relationships = Relation extends { Relationships: infer R } ? R : unknown + Relationships = Relation extends { Relationships: infer R } ? R : unknown, + ThrowOnError extends boolean = false > { url: URL headers: Record schema?: string signal?: AbortSignal fetch?: Fetch + shouldThrowOnError: ThrowOnError constructor( url: URL, @@ -21,16 +23,19 @@ export default class PostgrestQueryBuilder< headers = {}, schema, fetch, + shouldThrowOnError, }: { headers?: Record schema?: string fetch?: Fetch + shouldThrowOnError?: ThrowOnError } ) { this.url = url this.headers = headers this.schema = schema this.fetch = fetch + this.shouldThrowOnError = Boolean(shouldThrowOnError) as ThrowOnError } /** @@ -66,7 +71,14 @@ export default class PostgrestQueryBuilder< head?: boolean count?: 'exact' | 'planned' | 'estimated' } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + ResultOne[], + RelationName, + Relationships, + ThrowOnError + > { const method = head ? 'HEAD' : 'GET' // Remove whitespaces except when quoted let quoted = false @@ -94,7 +106,8 @@ export default class PostgrestQueryBuilder< schema: this.schema, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + shouldThrowOnError: this.shouldThrowOnError, + } as unknown as PostgrestBuilder) } // TODO(v3): Make `defaultToNull` consistent for both single & bulk inserts. @@ -103,14 +116,28 @@ export default class PostgrestQueryBuilder< options?: { count?: 'exact' | 'planned' | 'estimated' } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > insert( values: Row[], options?: { count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > /** * Perform an INSERT into the table or view. * @@ -146,7 +173,14 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > { const method = 'POST' const prefersHeaders = [] @@ -177,7 +211,8 @@ export default class PostgrestQueryBuilder< body: values, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + shouldThrowOnError: this.shouldThrowOnError, + } as unknown as PostgrestBuilder) } // TODO(v3): Make `defaultToNull` consistent for both single & bulk upserts. @@ -188,7 +223,14 @@ export default class PostgrestQueryBuilder< ignoreDuplicates?: boolean count?: 'exact' | 'planned' | 'estimated' } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > upsert( values: Row[], options?: { @@ -197,7 +239,14 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } - ): PostgrestFilterBuilder + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > /** * Perform an UPSERT on the table or view. Depending on the column(s) passed * to `onConflict`, `.upsert()` allows you to perform the equivalent of @@ -249,7 +298,14 @@ export default class PostgrestQueryBuilder< count?: 'exact' | 'planned' | 'estimated' defaultToNull?: boolean } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > { const method = 'POST' const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`] @@ -282,7 +338,8 @@ export default class PostgrestQueryBuilder< body: values, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + shouldThrowOnError: this.shouldThrowOnError, + } as unknown as PostgrestBuilder) } /** @@ -313,7 +370,14 @@ export default class PostgrestQueryBuilder< }: { count?: 'exact' | 'planned' | 'estimated' } = {} - ): PostgrestFilterBuilder { + ): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > { const method = 'PATCH' const prefersHeaders = [] if (this.headers['Prefer']) { @@ -332,7 +396,8 @@ export default class PostgrestQueryBuilder< body: values, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + shouldThrowOnError: this.shouldThrowOnError, + } as unknown as PostgrestBuilder) } /** @@ -358,7 +423,14 @@ export default class PostgrestQueryBuilder< count, }: { count?: 'exact' | 'planned' | 'estimated' - } = {}): PostgrestFilterBuilder { + } = {}): PostgrestFilterBuilder< + Schema, + Relation['Row'], + null, + RelationName, + Relationships, + ThrowOnError + > { const method = 'DELETE' const prefersHeaders = [] if (count) { @@ -376,6 +448,7 @@ export default class PostgrestQueryBuilder< schema: this.schema, fetch: this.fetch, allowEmpty: false, - } as unknown as PostgrestBuilder) + shouldThrowOnError: this.shouldThrowOnError, + } as unknown as PostgrestBuilder) } } diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index 87b7a4fa..f0e7deba 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -7,8 +7,9 @@ export default class PostgrestTransformBuilder< Row extends Record, Result, RelationName = unknown, - Relationships = unknown -> extends PostgrestBuilder { + Relationships = unknown, + ThrowOnError extends boolean = false +> extends PostgrestBuilder { /** * Perform a SELECT on the query result. * @@ -23,7 +24,14 @@ export default class PostgrestTransformBuilder< NewResultOne = GetResult >( columns?: Query - ): PostgrestTransformBuilder { + ): PostgrestTransformBuilder< + Schema, + Row, + NewResultOne[], + RelationName, + Relationships, + ThrowOnError + > { // Remove whitespaces except when quoted let quoted = false const cleanedColumns = (columns ?? '*') @@ -48,7 +56,8 @@ export default class PostgrestTransformBuilder< Row, NewResultOne[], RelationName, - Relationships + Relationships, + ThrowOnError > } @@ -188,11 +197,12 @@ export default class PostgrestTransformBuilder< * Query result must be one row (e.g. using `.limit(1)`), otherwise this * returns an error. */ - single< - ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never - >(): PostgrestBuilder { + single(): PostgrestBuilder< + ResultOne, + ThrowOnError + > { this.headers['Accept'] = 'application/vnd.pgrst.object+json' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -203,7 +213,7 @@ export default class PostgrestTransformBuilder< */ maybeSingle< ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never - >(): PostgrestBuilder { + >(): PostgrestBuilder { // Temporary partial fix for https://github.com/supabase/postgrest-js/issues/361 // Issue persists e.g. for `.insert([...]).select().maybeSingle()` if (this.method === 'GET') { @@ -212,23 +222,23 @@ 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 } /** * Return `data` as a string in CSV format. */ - csv(): PostgrestBuilder { + csv(): PostgrestBuilder { this.headers['Accept'] = 'text/csv' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** * Return `data` as an object in [GeoJSON](https://geojson.org) format. */ - geojson(): PostgrestBuilder> { + geojson(): PostgrestBuilder, ThrowOnError> { this.headers['Accept'] = 'application/geo+json' - return this as PostgrestBuilder> + return this as unknown as PostgrestBuilder, ThrowOnError> } /** @@ -270,7 +280,9 @@ export default class PostgrestTransformBuilder< buffers?: boolean wal?: boolean format?: 'json' | 'text' - } = {}): PostgrestBuilder[]> | PostgrestBuilder { + } = {}): + | PostgrestBuilder[], ThrowOnError> + | PostgrestBuilder { const options = [ analyze ? 'analyze' : null, verbose ? 'verbose' : null, @@ -285,8 +297,9 @@ 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[], ThrowOnError> + else return this as unknown as PostgrestBuilder } /** @@ -313,14 +326,16 @@ export default class PostgrestTransformBuilder< Row, NewResult, RelationName, - Relationships + Relationships, + ThrowOnError > { return this as unknown as PostgrestTransformBuilder< Schema, Row, NewResult, RelationName, - Relationships + Relationships, + ThrowOnError > } } diff --git a/test/basic.ts b/test/basic.ts index 7583fd7a..965569ae 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -731,8 +731,8 @@ test('maybeSingle w/ throwOnError', async () => { .from('messages') .select() .eq('message', 'i do not exist') - .throwOnError() .maybeSingle() + .throwOnError() .then(undefined, () => { passes = false }) diff --git a/test/index.test-d.ts b/test/index.test-d.ts index e467d2fd..7d07f792 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -183,6 +183,41 @@ const postgrest = new PostgrestClient(REST_URL) expectType[]>>(res) } +// throw on error on query +{ + const { data } = await postgrest.from('users').select('username').throwOnError() + expectType<{ username: string }[]>(data) +} + +// throw on error with maybeSingle is still nullable +{ + const { data } = await postgrest.from('users').select('username').maybeSingle().throwOnError() + expectType<{ username: string } | null>(data) +} + +// throw on error with rpc +{ + const { data: nullable } = await postgrest.rpc('get_status', { name_param: 'foo' }) + expectType<'ONLINE' | 'OFFLINE' | null>(nullable) + + const { data: notnull } = await postgrest.rpc('get_status', { name_param: 'foo' }).throwOnError() + expectType<'ONLINE' | 'OFFLINE'>(notnull) +} + +// queries without throw on error have nullable results +{ + const { data } = await postgrest.from('users').select('username') + expectType<{ username: string }[] | null>(data) +} + +// throw on error on client +{ + const strictClient = postgrest.throwOnError() + + const { data } = await strictClient.from('users').select('username') + expectType<{ username: string }[]>(data) +} + // one-to-one relationship { const { data: channels, error } = await postgrest