[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:
parent
de9d8ad231
commit
0cecd1243f
128
dal/Builder.ts
128
dal/Builder.ts
|
@ -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;
|
||||
}
|
||||
In(table: string): Builder {
|
||||
this.request.commands.push({ method: "In", args: [table] });
|
||||
this.request.commands.push({ method, args });
|
||||
})
|
||||
}
|
||||
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;
|
||||
}
|
||||
this.request = {
|
||||
id: 0,
|
||||
db: this.request.db,
|
||||
commands: [],
|
||||
};
|
||||
yield this.formatRow(row.r);
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -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]);
|
||||
|
|
61
dal/__test__/builder.test.ts
Normal file
61
dal/__test__/builder.test.ts
Normal 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);
|
||||
});
|
|
@ -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);
|
||||
});
|
BIN
dal/__test__/srv/test.sqlite
Normal file
BIN
dal/__test__/srv/test.sqlite
Normal file
Binary file not shown.
Loading…
Reference in a new issue