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