@supabase/postgrest-js
Version:
Isomorphic PostgREST client
301 lines (280 loc) • 9.66 kB
text/typescript
// @ts-ignore
import nodeFetch from '@supabase/node-fetch'
import type {
Fetch,
PostgrestSingleResponse,
PostgrestResponseSuccess,
CheckMatchingArrayTypes,
MergePartialResult,
IsValidResultOverride,
ClientServerOptions,
} from './types'
import PostgrestError from './PostgrestError'
import { ContainsNull } from './select-query-parser/types'
export default abstract class PostgrestBuilder<
ClientOptions extends ClientServerOptions,
Result,
ThrowOnError extends boolean = false
> implements
PromiseLike<
ThrowOnError extends true ? PostgrestResponseSuccess<Result> : PostgrestSingleResponse<Result>
>
{
protected method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE'
protected url: URL
protected headers: Headers
protected schema?: string
protected body?: unknown
protected shouldThrowOnError = false
protected signal?: AbortSignal
protected fetch: Fetch
protected isMaybeSingle: boolean
constructor(builder: {
method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE'
url: URL
headers: HeadersInit
schema?: string
body?: unknown
shouldThrowOnError?: boolean
signal?: AbortSignal
fetch?: Fetch
isMaybeSingle?: boolean
}) {
this.method = builder.method
this.url = builder.url
this.headers = new Headers(builder.headers)
this.schema = builder.schema
this.body = builder.body
this.shouldThrowOnError = builder.shouldThrowOnError ?? false
this.signal = builder.signal
this.isMaybeSingle = builder.isMaybeSingle ?? false
if (builder.fetch) {
this.fetch = builder.fetch
} else if (typeof fetch === 'undefined') {
this.fetch = nodeFetch
} else {
this.fetch = fetch
}
}
/**
* 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 & PostgrestBuilder<ClientOptions, Result, true> {
this.shouldThrowOnError = true
return this as this & PostgrestBuilder<ClientOptions, Result, true>
}
/**
* Set an HTTP header for the request.
*/
setHeader(name: string, value: string): this {
this.headers = new Headers(this.headers)
this.headers.set(name, value)
return this
}
then<
TResult1 = ThrowOnError extends true
? PostgrestResponseSuccess<Result>
: PostgrestSingleResponse<Result>,
TResult2 = never
>(
onfulfilled?:
| ((
value: ThrowOnError extends true
? PostgrestResponseSuccess<Result>
: PostgrestSingleResponse<Result>
) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): PromiseLike<TResult1 | TResult2> {
// https://postgrest.org/en/stable/api.html#switching-schemas
if (this.schema === undefined) {
// skip
} else if (['GET', 'HEAD'].includes(this.method)) {
this.headers.set('Accept-Profile', this.schema)
} else {
this.headers.set('Content-Profile', this.schema)
}
if (this.method !== 'GET' && this.method !== 'HEAD') {
this.headers.set('Content-Type', 'application/json')
}
// NOTE: Invoke w/o `this` to avoid illegal invocation error.
// https://github.com/supabase/postgrest-js/pull/247
const _fetch = this.fetch
let res = _fetch(this.url.toString(), {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.body),
signal: this.signal,
}).then(async (res) => {
let error = null
let data = null
let count: number | null = null
let status = res.status
let statusText = res.statusText
if (res.ok) {
if (this.method !== 'HEAD') {
const body = await res.text()
if (body === '') {
// Prefer: return=minimal
} else if (this.headers.get('Accept') === 'text/csv') {
data = body
} else if (
this.headers.get('Accept') &&
this.headers.get('Accept')?.includes('application/vnd.pgrst.plan+text')
) {
data = body
} else {
data = JSON.parse(body)
}
}
const countHeader = this.headers.get('Prefer')?.match(/count=(exact|planned|estimated)/)
const contentRange = res.headers.get('content-range')?.split('/')
if (countHeader && contentRange && contentRange.length > 1) {
count = parseInt(contentRange[1])
}
// Temporary partial fix for https://github.com/supabase/postgrest-js/issues/361
// Issue persists e.g. for `.insert([...]).select().maybeSingle()`
if (this.isMaybeSingle && this.method === 'GET' && Array.isArray(data)) {
if (data.length > 1) {
error = {
// https://github.com/PostgREST/postgrest/blob/a867d79c42419af16c18c3fb019eba8df992626f/src/PostgREST/Error.hs#L553
code: 'PGRST116',
details: `Results contain ${data.length} rows, application/vnd.pgrst.object+json requires 1 row`,
hint: null,
message: 'JSON object requested, multiple (or no) rows returned',
}
data = null
count = null
status = 406
statusText = 'Not Acceptable'
} else if (data.length === 1) {
data = data[0]
} else {
data = null
}
}
} else {
const body = await res.text()
try {
error = JSON.parse(body)
// Workaround for https://github.com/supabase/postgrest-js/issues/295
if (Array.isArray(error) && res.status === 404) {
data = []
error = null
status = 200
statusText = 'OK'
}
} catch {
// Workaround for https://github.com/supabase/postgrest-js/issues/295
if (res.status === 404 && body === '') {
status = 204
statusText = 'No Content'
} else {
error = {
message: body,
}
}
}
if (error && this.isMaybeSingle && error?.details?.includes('0 rows')) {
error = null
status = 200
statusText = 'OK'
}
if (error && this.shouldThrowOnError) {
throw new PostgrestError(error)
}
}
const postgrestResponse = {
error,
data,
count,
status,
statusText,
}
return postgrestResponse
})
if (!this.shouldThrowOnError) {
res = res.catch((fetchError) => ({
error: {
message: `${fetchError?.name ?? 'FetchError'}: ${fetchError?.message}`,
details: `${fetchError?.stack ?? ''}`,
hint: '',
code: `${fetchError?.code ?? ''}`,
},
data: null,
count: null,
status: 0,
statusText: '',
}))
}
return res.then(onfulfilled, onrejected)
}
/**
* Override the type of the returned `data`.
*
* @typeParam NewResult - The new result type to override with
* @deprecated Use overrideTypes<yourType, { merge: false }>() method at the end of your call chain instead
*/
returns<NewResult>(): PostgrestBuilder<
ClientOptions,
CheckMatchingArrayTypes<Result, NewResult>,
ThrowOnError
> {
/* istanbul ignore next */
return this as unknown as PostgrestBuilder<
ClientOptions,
CheckMatchingArrayTypes<Result, NewResult>,
ThrowOnError
>
}
/**
* 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<
ClientOptions,
IsValidResultOverride<Result, NewResult, false, false> extends true
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
ContainsNull<Result> extends true
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
: MergePartialResult<NewResult, Result, Options>
: CheckMatchingArrayTypes<Result, NewResult>,
ThrowOnError
> {
return this as unknown as PostgrestBuilder<
ClientOptions,
IsValidResultOverride<Result, NewResult, false, false> extends true
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
ContainsNull<Result> extends true
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
: MergePartialResult<NewResult, Result, Options>
: CheckMatchingArrayTypes<Result, NewResult>,
ThrowOnError
>
}
}