dal/client/Builder.ts
Anton Nesterov e62a155e04
[fix] handle errors
Signed-off-by: Anton Nesterov <anton@demiurg.io>
2024-09-03 01:11:17 +02:00

225 lines
5.8 KiB
TypeScript

import type { Request, ExecResult, IError } from "./Protocol";
import {
METHODS,
encodeRequest,
decodeResponse,
decodeRowsIterator,
} from "./Protocol";
type Primitive = string | number | boolean | null;
interface Filter extends Record<string, unknown> {
$eq?: Primitive;
$ne?: Primitive;
$gt?: Primitive;
$gte?: Primitive;
$lt?: Primitive;
$lte?: Primitive;
$in?: Primitive[];
$nin?: Primitive[];
$like?: string;
$nlike?: string;
$glob?: string;
$between?: [Primitive, Primitive];
$nbetween?: [Primitive, Primitive];
}
interface FindFilter {
[key: string]: Primitive | Filter | Filter[] | undefined;
}
type JoinCondition = "inner" | "left" | "cross" | "full outer";
type JoinFilter = {
$for: string;
$do: FindFilter;
$as?: JoinCondition;
};
type SortOptions = Record<string, 1 | -1 | "asc" | "desc">;
type Options = {
database: string;
url: string;
headers?: Record<string, string>;
};
export default class Builder<I extends abstract new (...args: any) => any> {
protected request: Request;
protected url: string;
protected dtoTemplate: new (...args: any) => any = Object;
protected methodCalls: Map<string, unknown[]> = new Map(); // one call per method
protected headerRow: unknown[] | null = null;
protected httpHeaders: Record<string, string> = {};
constructor(opts: Options) {
this.request = {
id: 0,
db: opts.database,
commands: [],
};
this.url = opts.url;
if (opts.headers) {
this.httpHeaders = opts.headers;
}
}
protected formatRequest(): void {
this.request.commands = [];
METHODS.forEach((method) => {
const args = this.methodCalls.get(method);
if (!args) {
return;
}
this.request.commands.push({ method, args });
});
}
protected formatRow(data: unknown[]) {
if (!this.dtoTemplate || this.dtoTemplate === Object) {
return data;
}
const instance = new this.dtoTemplate(data);
for (const idx in this.headerRow!) {
const header = this.headerRow[idx] as string;
if (header in instance) {
instance[header] = data[idx];
}
}
return instance;
}
Raw(sql: string, ...values: unknown[]): Builder<I> {
this.methodCalls.set("Raw", [{ s: sql, v: values }]);
return this;
}
In(table: string): Builder<I> {
this.methodCalls.set("In", [table]);
return this;
}
Find(filter: FindFilter): Builder<I> {
this.methodCalls.set("Find", [filter]);
return this;
}
Select(fields: Record<string, any>): Builder<I> {
this.methodCalls.set("Select", [fields]);
return this;
}
Fields(fields: Record<string, any>): Builder<I> {
this.Select(fields);
return this;
}
Join(...joins: JoinFilter[]): Builder<I> {
this.methodCalls.set("Join", joins);
return this;
}
Group(fields: string[]): Builder<I> {
this.methodCalls.set("Group", fields);
return this;
}
Sort(fields: SortOptions): Builder<I> {
this.methodCalls.set("Sort", [fields]);
return this;
}
Limit(limit: number): Builder<I> {
this.methodCalls.set("Limit", [limit]);
return this;
}
Offset(offset: number): Builder<I> {
this.methodCalls.set("Offset", [offset]);
return this;
}
Delete(): Builder<I> {
this.methodCalls.set("Delete", []);
return this;
}
Insert(...data: Record<string, unknown>[]): Builder<I> {
this.methodCalls.set("Insert", data);
return this;
}
Set(data: Record<string, unknown>): Builder<I> {
this.methodCalls.set("Set", [data]);
return this;
}
Update(data: Record<string, unknown>): Builder<I> {
this.Set(data);
return this;
}
OnConflict(...fields: string[]): Builder<I> {
this.methodCalls.set("OnConflict", fields);
return this;
}
DoUpdate(...fields: string[]): Builder<I> {
this.methodCalls.delete("DoNothing");
this.methodCalls.set("DoUpdate", fields);
return this;
}
DoNothing(): Builder<I> {
this.methodCalls.delete("DoUpdate");
this.methodCalls.set("DoNothing", []);
return this;
}
Tx(): Builder<I> {
this.methodCalls.set("Tx", []);
return this;
}
As<T extends new (...args: any) => any>(template: T): Builder<T> {
this.dtoTemplate = template;
return this;
}
async *Rows<T = InstanceType<I>>(): AsyncGenerator<[T, IError]> {
this.formatRequest();
const response = await fetch(this.url, {
method: "POST",
body: new Blob([encodeRequest(this.request)]),
headers: {
"Content-Type": "application/x-msgpack",
...this.httpHeaders,
},
});
if (response.status !== 200) {
return [[], await response.text()];
}
const iterator = decodeRowsIterator(response.body!);
for await (const result of iterator) {
const [row, err] = result;
if (err) {
yield [{} as T, err];
return;
}
if (this.headerRow === null) {
this.headerRow = row.r;
continue;
}
yield [this.formatRow(row.r), null];
}
}
async Query<T = InstanceType<I>>(): Promise<T[]> {
const rows = this.Rows();
const result: T[] = [];
for await (const res of rows) {
const [row, error] = res;
if (error) {
if (String(error).includes("RangeError")) {
break;
}
throw new Error(error);
}
result.push(row);
}
return result;
}
async Exec(): Promise<[ExecResult, IError]> {
this.formatRequest();
const response = await fetch(this.url, {
method: "POST",
body: new Blob([encodeRequest(this.request)]),
headers: {
"Content-Type": "application/x-msgpack",
...this.httpHeaders,
},
});
if (response.status !== 200) {
throw new Error(await response.text());
}
const buf = await response.arrayBuffer();
return decodeResponse(new Uint8Array(buf))!;
}
}