@miuiu/postgrest
Version:
Isomorphic PostgREST client
283 lines (269 loc) • 7.88 kB
text/typescript
import PostgrestBuilder from "./PostgrestBuilder";
import { GetResult } from "./select-query-parser";
import { GenericSchema } from "./types";
export default class PostgrestTransformBuilder<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Result,
> extends PostgrestBuilder<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, Query>,
>(columns?: Query): PostgrestTransformBuilder<Schema, Row, NewResultOne[]> {
// 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);
if (this.headers["Prefer"]) {
this.headers["Prefer"] += ",";
}
this.headers["Prefer"] += "return=representation";
return this as unknown as PostgrestTransformBuilder<
Schema,
Row,
NewResultOne[]
>;
}
order<ColumnName extends string & keyof Row>(
column: ColumnName,
options?: {
ascending?: boolean;
nullsFirst?: boolean;
foreignTable?: undefined;
},
): this;
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 foreign tables, but it doesn't affect the ordering of the
* current table.
*
* @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.foreignTable - Set this to order a foreign table by foreign
* columns
*/
order(
column: string,
{
ascending = true,
nullsFirst,
foreignTable,
}: {
ascending?: boolean;
nullsFirst?: boolean;
foreignTable?: string;
} = {},
): this {
const key = foreignTable ? `${foreignTable}.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.foreignTable - Set this to limit rows of foreign tables
* instead of the current table
*/
limit(count: number, { foreignTable }: { foreignTable?: string } = {}): this {
const key =
typeof foreignTable === "undefined" ? "limit" : `${foreignTable}.limit`;
this.url.searchParams.set(key, `${count}`);
return this;
}
/**
* Limit the query result by `from` and `to` inclusively.
*
* @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.foreignTable - Set this to limit rows of foreign tables
* instead of the current table
*/
range(
from: number,
to: number,
{ foreignTable }: { foreignTable?: string } = {},
): this {
const keyOffset =
typeof foreignTable === "undefined" ? "offset" : `${foreignTable}.offset`;
const keyLimit =
typeof foreignTable === "undefined" ? "limit" : `${foreignTable}.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<ResultOne> {
this.headers["Accept"] = "application/vnd.pgrst.object+json";
return this as PostgrestBuilder<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<ResultOne | null> {
this.headers["Accept"] = "application/vnd.pgrst.object+json";
this.allowEmpty = true;
return this as PostgrestBuilder<ResultOne | null>;
}
/**
* Return `data` as a string in CSV format.
*/
csv(): PostgrestBuilder<string> {
this.headers["Accept"] = "text/csv";
return this as PostgrestBuilder<string>;
}
/**
* Return `data` as an object in [GeoJSON](https://geojson.org) format.
*/
geojson(): PostgrestBuilder<Record<string, unknown>> {
this.headers["Accept"] = "application/geo+json";
return this as PostgrestBuilder<Record<string, unknown>>;
}
/**
* Return `data` as the EXPLAIN plan for the query.
*
* @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";
} = {}):
| PostgrestBuilder<Record<string, unknown>[]>
| PostgrestBuilder<string> {
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["Accept"];
this.headers[
"Accept"
] = `application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};`;
if (format === "json")
return this as PostgrestBuilder<Record<string, unknown>[]>;
else return this as PostgrestBuilder<string>;
}
/**
* Rollback the query.
*
* `data` will still be returned, but the query is not committed.
*/
rollback(): this {
if ((this.headers["Prefer"] ?? "").trim().length > 0) {
this.headers["Prefer"] += ",tx=rollback";
} else {
this.headers["Prefer"] = "tx=rollback";
}
return this;
}
/**
* Override the type of the returned `data`.
*
* @typeParam NewResult - The new result type to override with
*/
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult> {
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult>;
}
}