@supabase/postgrest-js
Version:
Isomorphic PostgREST client
369 lines (354 loc) • 11.9 kB
text/typescript
import PostgrestBuilder from './PostgrestBuilder'
import { InvalidMethodError } from './PostgrestFilterBuilder'
import { GetResult } from './select-query-parser/result'
import {
GenericSchema,
CheckMatchingArrayTypes,
ClientServerOptions,
MaxAffectedEnabled,
} from './types'
export default class PostgrestTransformBuilder<
ClientOptions extends ClientServerOptions,
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Result,
RelationName = unknown,
Relationships = unknown,
Method = unknown
> extends PostgrestBuilder<ClientOptions, Result> {
/**
* Perform a SELECT on the query result.
*
* By default, `.insert()`, `.update()`, `.upsert()`, and `.delete()` do not
* return modified rows. By calling this method, modified rows are returned in
* `data`.
*
* @param columns - The columns to retrieve, separated by commas
*/
select<
Query extends string = '*',
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query, ClientOptions>
>(
columns?: Query
): PostgrestTransformBuilder<
ClientOptions,
Schema,
Row,
NewResultOne[],
RelationName,
Relationships,
Method
> {
// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = (columns ?? '*')
.split('')
.map((c) => {
if (/\s/.test(c) && !quoted) {
return ''
}
if (c === '"') {
quoted = !quoted
}
return c
})
.join('')
this.url.searchParams.set('select', cleanedColumns)
this.headers.append('Prefer', 'return=representation')
return this as unknown as PostgrestTransformBuilder<
ClientOptions,
Schema,
Row,
NewResultOne[],
RelationName,
Relationships,
Method
>
}
order<ColumnName extends string & keyof Row>(
column: ColumnName,
options?: { ascending?: boolean; nullsFirst?: boolean; referencedTable?: undefined }
): this
order(
column: string,
options?: { ascending?: boolean; nullsFirst?: boolean; referencedTable?: string }
): this
/**
* @deprecated Use `options.referencedTable` instead of `options.foreignTable`
*/
order<ColumnName extends string & keyof Row>(
column: ColumnName,
options?: { ascending?: boolean; nullsFirst?: boolean; foreignTable?: undefined }
): this
/**
* @deprecated Use `options.referencedTable` instead of `options.foreignTable`
*/
order(
column: string,
options?: { ascending?: boolean; nullsFirst?: boolean; foreignTable?: string }
): this
/**
* Order the query result by `column`.
*
* You can call this method multiple times to order by multiple columns.
*
* You can order referenced tables, but it only affects the ordering of the
* parent table if you use `!inner` in the query.
*
* @param column - The column to order by
* @param options - Named parameters
* @param options.ascending - If `true`, the result will be in ascending order
* @param options.nullsFirst - If `true`, `null`s appear first. If `false`,
* `null`s appear last.
* @param options.referencedTable - Set this to order a referenced table by
* its columns
* @param options.foreignTable - Deprecated, use `options.referencedTable`
* instead
*/
order(
column: string,
{
ascending = true,
nullsFirst,
foreignTable,
referencedTable = foreignTable,
}: {
ascending?: boolean
nullsFirst?: boolean
foreignTable?: string
referencedTable?: string
} = {}
): this {
const key = referencedTable ? `${referencedTable}.order` : 'order'
const existingOrder = this.url.searchParams.get(key)
this.url.searchParams.set(
key,
`${existingOrder ? `${existingOrder},` : ''}${column}.${ascending ? 'asc' : 'desc'}${
nullsFirst === undefined ? '' : nullsFirst ? '.nullsfirst' : '.nullslast'
}`
)
return this
}
/**
* Limit the query result by `count`.
*
* @param count - The maximum number of rows to return
* @param options - Named parameters
* @param options.referencedTable - Set this to limit rows of referenced
* tables instead of the parent table
* @param options.foreignTable - Deprecated, use `options.referencedTable`
* instead
*/
limit(
count: number,
{
foreignTable,
referencedTable = foreignTable,
}: { foreignTable?: string; referencedTable?: string } = {}
): this {
const key = typeof referencedTable === 'undefined' ? 'limit' : `${referencedTable}.limit`
this.url.searchParams.set(key, `${count}`)
return this
}
/**
* Limit the query result by starting at an offset `from` and ending at the offset `to`.
* Only records within this range are returned.
* This respects the query order and if there is no order clause the range could behave unexpectedly.
* The `from` and `to` values are 0-based and inclusive: `range(1, 3)` will include the second, third
* and fourth rows of the query.
*
* @param from - The starting index from which to limit the result
* @param to - The last index to which to limit the result
* @param options - Named parameters
* @param options.referencedTable - Set this to limit rows of referenced
* tables instead of the parent table
* @param options.foreignTable - Deprecated, use `options.referencedTable`
* instead
*/
range(
from: number,
to: number,
{
foreignTable,
referencedTable = foreignTable,
}: { foreignTable?: string; referencedTable?: string } = {}
): this {
const keyOffset =
typeof referencedTable === 'undefined' ? 'offset' : `${referencedTable}.offset`
const keyLimit = typeof referencedTable === 'undefined' ? 'limit' : `${referencedTable}.limit`
this.url.searchParams.set(keyOffset, `${from}`)
// Range is inclusive, so add 1
this.url.searchParams.set(keyLimit, `${to - from + 1}`)
return this
}
/**
* Set the AbortSignal for the fetch request.
*
* @param signal - The AbortSignal to use for the fetch request
*/
abortSignal(signal: AbortSignal): this {
this.signal = signal
return this
}
/**
* Return `data` as a single object instead of an array of objects.
*
* 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<
ClientOptions,
ResultOne
> {
this.headers.set('Accept', 'application/vnd.pgrst.object+json')
return this as unknown as PostgrestBuilder<ClientOptions, ResultOne>
}
/**
* Return `data` as a single object instead of an array of objects.
*
* Query result must be zero or one row (e.g. using `.limit(1)`), otherwise
* this returns an error.
*/
maybeSingle<
ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never
>(): PostgrestBuilder<ClientOptions, ResultOne | null> {
// Temporary partial fix for https://github.com/supabase/postgrest-js/issues/361
// Issue persists e.g. for `.insert([...]).select().maybeSingle()`
if (this.method === 'GET') {
this.headers.set('Accept', 'application/json')
} else {
this.headers.set('Accept', 'application/vnd.pgrst.object+json')
}
this.isMaybeSingle = true
return this as unknown as PostgrestBuilder<ClientOptions, ResultOne | null>
}
/**
* Return `data` as a string in CSV format.
*/
csv(): PostgrestBuilder<ClientOptions, string> {
this.headers.set('Accept', 'text/csv')
return this as unknown as PostgrestBuilder<ClientOptions, string>
}
/**
* Return `data` as an object in [GeoJSON](https://geojson.org) format.
*/
geojson(): PostgrestBuilder<ClientOptions, Record<string, unknown>> {
this.headers.set('Accept', 'application/geo+json')
return this as unknown as PostgrestBuilder<ClientOptions, Record<string, unknown>>
}
/**
* Return `data` as the EXPLAIN plan for the query.
*
* You need to enable the
* [db_plan_enabled](https://supabase.com/docs/guides/database/debugging-performance#enabling-explain)
* setting before using this method.
*
* @param options - Named parameters
*
* @param options.analyze - If `true`, the query will be executed and the
* actual run time will be returned
*
* @param options.verbose - If `true`, the query identifier will be returned
* and `data` will include the output columns of the query
*
* @param options.settings - If `true`, include information on configuration
* parameters that affect query planning
*
* @param options.buffers - If `true`, include information on buffer usage
*
* @param options.wal - If `true`, include information on WAL record generation
*
* @param options.format - The format of the output, can be `"text"` (default)
* or `"json"`
*/
explain({
analyze = false,
verbose = false,
settings = false,
buffers = false,
wal = false,
format = 'text',
}: {
analyze?: boolean
verbose?: boolean
settings?: boolean
buffers?: boolean
wal?: boolean
format?: 'json' | 'text'
} = {}) {
const options = [
analyze ? 'analyze' : null,
verbose ? 'verbose' : null,
settings ? 'settings' : null,
buffers ? 'buffers' : null,
wal ? 'wal' : null,
]
.filter(Boolean)
.join('|')
// An Accept header can carry multiple media types but postgrest-js always sends one
const forMediatype = this.headers.get('Accept') ?? 'application/json'
this.headers.set(
'Accept',
`application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};`
)
if (format === 'json') {
return this as unknown as PostgrestBuilder<ClientOptions, Record<string, unknown>[]>
} else {
return this as unknown as PostgrestBuilder<ClientOptions, string>
}
}
/**
* Rollback the query.
*
* `data` will still be returned, but the query is not committed.
*/
rollback(): this {
this.headers.append('Prefer', 'tx=rollback')
return this
}
/**
* 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>(): PostgrestTransformBuilder<
ClientOptions,
Schema,
Row,
CheckMatchingArrayTypes<Result, NewResult>,
RelationName,
Relationships,
Method
> {
return this as unknown as PostgrestTransformBuilder<
ClientOptions,
Schema,
Row,
CheckMatchingArrayTypes<Result, NewResult>,
RelationName,
Relationships,
Method
>
}
/**
* Set the maximum number of rows that can be affected by the query.
* Only available in PostgREST v13+ and only works with PATCH and DELETE methods.
*
* @param value - The maximum number of rows that can be affected
*/
maxAffected(value: number): MaxAffectedEnabled<ClientOptions['PostgrestVersion']> extends true
? // TODO: update the RPC case to only work on RPC that returns SETOF rows
Method extends 'PATCH' | 'DELETE' | 'RPC'
? this
: InvalidMethodError<'maxAffected method only available on update or delete'>
: InvalidMethodError<'maxAffected method only available on postgrest 13+'> {
this.headers.append('Prefer', 'handling=strict')
this.headers.append('Prefer', `max-affected=${value}`)
return this as unknown as MaxAffectedEnabled<ClientOptions['PostgrestVersion']> extends true
? Method extends 'PATCH' | 'DELETE' | 'RPC'
? this
: InvalidMethodError<'maxAffected method only available on update or delete'>
: InvalidMethodError<'maxAffected method only available on postgrest 13+'>
}
}