import type { Request, ExecResult, IError } from "./Protocol"; import { METHODS, encodeRequest, decodeResponse, decodeRowsIterator, } from "./Protocol"; type Primitive = string | number | boolean | null; interface Filter extends Record { $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; type Options = { database: string; url: string; headers?: Record; }; export default class Builder any> { protected request: Request; protected url: string; protected dtoTemplate: new (...args: any) => any = Object; protected methodCalls: Map = new Map(); // one call per method protected headerRow: unknown[] | null = null; protected httpHeaders: Record = {}; 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 { this.methodCalls.set("Raw", [{ s: sql, v: values }]); return this; } In(table: string): Builder { this.methodCalls.set("In", [table]); return this; } Find(filter: FindFilter): Builder { this.methodCalls.set("Find", [filter]); return this; } Select(fields: Record): Builder { this.methodCalls.set("Select", [fields]); return this; } Fields(fields: Record): Builder { this.Select(fields); return this; } Join(...joins: JoinFilter[]): Builder { this.methodCalls.set("Join", joins); return this; } Group(fields: string[]): Builder { this.methodCalls.set("Group", fields); return this; } Sort(fields: SortOptions): Builder { this.methodCalls.set("Sort", [fields]); return this; } Limit(limit: number): Builder { this.methodCalls.set("Limit", [limit]); return this; } Offset(offset: number): Builder { this.methodCalls.set("Offset", [offset]); return this; } Delete(): Builder { this.methodCalls.set("Delete", []); return this; } Insert(...data: Record[]): Builder { this.methodCalls.set("Insert", data); return this; } Set(data: Record): Builder { this.methodCalls.set("Set", [data]); return this; } Update(data: Record): Builder { this.Set(data); return this; } OnConflict(...fields: string[]): Builder { this.methodCalls.set("OnConflict", fields); return this; } DoUpdate(...fields: string[]): Builder { this.methodCalls.delete("DoNothing"); this.methodCalls.set("DoUpdate", fields); return this; } DoNothing(): Builder { this.methodCalls.delete("DoUpdate"); this.methodCalls.set("DoNothing", []); return this; } Tx(): Builder { this.methodCalls.set("Tx", []); return this; } As any>(template: T): Builder { this.dtoTemplate = template; return this; } async *Rows>(): 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>(): Promise { 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))!; } }