[feat] generics for TS builder; [feat] support row formating as DTO class

Signed-off-by: Anton Nesterov <anton@demiurg.io>
This commit is contained in:
Anton Nesterov 2024-08-15 15:02:47 +02:00
parent de9d8ad231
commit 0cecd1243f
No known key found for this signature in database
GPG key ID: 59121E8AE2851FB5
5 changed files with 144 additions and 70 deletions

View file

@ -36,9 +36,15 @@ type Options = {
url: string;
};
export default class Builder {
export default class Builder <
I extends abstract new (...args: any) => any,
>{
private request: Request;
private url: string;
private dtoTemplate: new (...args: any) => any = Object;
private methodCalls: Map<string, unknown[]> = new Map(); // one call per method
private headerRow: unknown[] | null = null;
constructor(opts: Options) {
this.request = {
id: 0,
@ -47,78 +53,97 @@ export default class Builder {
};
this.url = opts.url;
}
private format(): void {
this.request.commands = METHODS.map((method) => {
const command = this.request.commands.find((command) => command.method === method);
return command;
}).filter(Boolean) as Request["commands"];
private formatRequest(): void {
this.request.commands = []
METHODS.forEach((method) => {
const args = this.methodCalls.get(method);
if (!args) {
return;
}
this.request.commands.push({ method, args });
})
}
In(table: string): Builder {
this.request.commands.push({ method: "In", args: [table] });
private formatRow(data: unknown[]){
if (!this.dtoTemplate) {
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;
}
In(table: string): Builder<I> {
this.methodCalls.set("In", [table]);
return this;
}
Find(filter: FindFilter): Builder {
this.request.commands.push({ method: "Find", args: [filter] });
Find(filter: FindFilter): Builder<I> {
this.methodCalls.set("Find", [filter]);
return this;
}
Select(fields: string[]): Builder {
this.request.commands.push({ method: "Select", args: fields });
Select(fields: string[]): Builder<I> {
this.methodCalls.set("Select", fields);
return this;
}
Fields(fields: string[]): Builder {
Fields(fields: string[]): Builder<I> {
this.Select(fields);
return this;
}
Join(...joins: JoinFilter[]): Builder {
this.request.commands.push({ method: "Join", args: joins });
Join(...joins: JoinFilter[]): Builder<I> {
this.methodCalls.set("Join", joins);
return this;
}
Group(fields: string[]): Builder {
this.request.commands.push({ method: "Group", args: fields });
Group(fields: string[]): Builder<I> {
this.methodCalls.set("Group", fields);
return this;
}
Sort(fields: SortOptions): Builder {
this.request.commands.push({ method: "Sort", args: fields });
Sort(fields: SortOptions): Builder<I> {
this.methodCalls.set("Sort", [fields]);
return this;
}
Limit(limit: number): Builder {
this.request.commands.push({ method: "Limit", args: [limit] });
Limit(limit: number): Builder<I> {
this.methodCalls.set("Limit", [limit]);
return this;
}
Offset(offset: number): Builder {
this.request.commands.push({ method: "Offset", args: [offset] });
Offset(offset: number): Builder<I> {
this.methodCalls.set("Offset", [offset]);
return this;
}
Delete(): Builder {
this.request.commands.push({ method: "Delete", args: [] });
Delete(): Builder<I> {
this.methodCalls.set("Delete", []);
return this;
}
Insert(data: Record<string, unknown>): Builder {
this.request.commands.push({ method: "Insert", args: [data] });
Insert(data: Record<string, unknown>): Builder<I> {
this.methodCalls.set("Insert", [data]);
return this;
}
Set(data: Record<string, unknown>): Builder {
this.request.commands.push({ method: "Set", args: [data] });
Set(data: Record<string, unknown>): Builder<I> {
this.methodCalls.set("Set", [data]);
return this;
}
Update(data: Record<string, unknown>): Builder {
Update(data: Record<string, unknown>): Builder<I> {
this.Set(data);
return this;
}
OnConflict(...fields: string[]): Builder {
this.request.commands.push({ method: "OnConflict", args: fields });
OnConflict(...fields: string[]): Builder<I> {
this.methodCalls.set("OnConflict", fields);
return this;
}
DoUpdate(...fields: string[]): Builder {
this.request.commands.push({ method: "DoUpdate", args: fields });
DoUpdate(...fields: string[]): Builder<I> {
this.methodCalls.delete("DoNothing");
this.methodCalls.set("DoUpdate", fields);
return this;
}
DoNothing(): Builder {
this.request.commands.push({ method: "DoNothing", args: [] });
DoNothing(): Builder<I> {
this.methodCalls.delete("DoUpdate");
this.methodCalls.set("DoNothing", []);
return this;
}
async *Rows() {
this.format();
async *Rows<T = InstanceType<I>>(): AsyncGenerator<T> {
this.formatRequest();
const response = await fetch(this.url, {
method: "POST",
body: new Blob([encodeRequest(this.request)]),
@ -130,14 +155,27 @@ export default class Builder {
throw new Error(await response.text());
}
for await (const row of decodeRowsIterator(response.body!)) {
yield row;
const iterator = decodeRowsIterator(response.body!);
for await (const row of iterator) {
if (this.headerRow === null) {
this.headerRow = row.r;
await iterator.next();
continue;
}
yield this.formatRow(row.r);
}
this.request = {
id: 0,
db: this.request.db,
commands: [],
};
}
As<T extends new (...args: any) => any>(template: T): Builder<T> {
this.dtoTemplate = template;
return this;
}
async Query<T = InstanceType<I>>(): Promise<T[]> {
const rows = this.Rows();
const result = [];
for await (const row of rows) {
result.push(row);
}
return result
}
}

View file

@ -55,7 +55,6 @@ export async function *decodeRowsIterator(stream: ReadableStream<Uint8Array>): A
for (;;) {
const { value, done } = await reader.read();
if (done) {
console.log("done");
break;
}
buf = new Uint8Array([...buf, ...value]);

View file

@ -0,0 +1,61 @@
import { test, expect } from "bun:test";
import { DAL } from ".."
const options = {
database: "test.sqlite",
url: "http://localhost:8111",
}
class DTO {
id: number = 0;
name: string = "";
data: string = "";
age: number | undefined;
}
test("Rows iter, no format", async () => {
const dal = new DAL(options);
const rows = dal
.In("test t")
.Find({
id: 1,
})
.Rows<any[]>();
for await (const row of rows) {
console.log(row);
expect(row.length).toBe(3);
}
expect(true).toBe(true);
});
test("Rows iter, format", async () => {
const dal = new DAL(options);
const rows = dal
.In("test t")
.Find({
id: 1,
})
.As(DTO)
.Rows();
for await (const row of rows) {
console.log(row);
expect(row.id).toBe(1);
}
expect(true).toBe(true);
});
test("Query format", async () => {
const dal = new DAL(options);
const rows = await dal
.In("test t")
.Find({
id: 1,
})
.As(DTO)
.Query();
for (const row of rows) {
expect(row.id).toBeDefined();
expect(row.age).toBeUndefined();
}
expect(true).toBe(true);
});

View file

@ -1,24 +0,0 @@
import { test, expect } from "bun:test";
import { DAL } from ".."
const options = {
database: "test.sqlite",
url: "http://localhost:8111",
}
test("Rows iter", async () => {
const dal = new DAL(options);
const rows = dal
.In("test t")
.Find({
id: 1,
})
.Rows();
for await (const row of rows) {
// console.log(row);
//@ts-ignore
expect(row.r.length).toBe(3);
}
expect(true).toBe(true);
});

Binary file not shown.