From 0cecd1243f2044df5055fd00454a4e0139d405e4 Mon Sep 17 00:00:00 2001 From: Anton Nesterov Date: Thu, 15 Aug 2024 15:02:47 +0200 Subject: [PATCH] [feat] generics for TS builder; [feat] support row formating as DTO class Signed-off-by: Anton Nesterov --- dal/Builder.ts | 128 ++++++++++++++++++++++------------ dal/Protocol.ts | 1 - dal/__test__/builder.test.ts | 61 ++++++++++++++++ dal/__test__/protocol.test.ts | 24 ------- dal/__test__/srv/test.sqlite | Bin 0 -> 8192 bytes 5 files changed, 144 insertions(+), 70 deletions(-) create mode 100644 dal/__test__/builder.test.ts delete mode 100644 dal/__test__/protocol.test.ts create mode 100644 dal/__test__/srv/test.sqlite diff --git a/dal/Builder.ts b/dal/Builder.ts index 98ed72a..1a096e7 100644 --- a/dal/Builder.ts +++ b/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 = 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 { + this.methodCalls.set("In", [table]); return this; } - Find(filter: FindFilter): Builder { - this.request.commands.push({ method: "Find", args: [filter] }); + Find(filter: FindFilter): Builder { + this.methodCalls.set("Find", [filter]); return this; } - Select(fields: string[]): Builder { - this.request.commands.push({ method: "Select", args: fields }); + Select(fields: string[]): Builder { + this.methodCalls.set("Select", fields); return this; } - Fields(fields: string[]): Builder { + Fields(fields: string[]): Builder { this.Select(fields); return this; } - Join(...joins: JoinFilter[]): Builder { - this.request.commands.push({ method: "Join", args: joins }); + Join(...joins: JoinFilter[]): Builder { + this.methodCalls.set("Join", joins); return this; } - Group(fields: string[]): Builder { - this.request.commands.push({ method: "Group", args: fields }); + Group(fields: string[]): Builder { + this.methodCalls.set("Group", fields); return this; } - Sort(fields: SortOptions): Builder { - this.request.commands.push({ method: "Sort", args: fields }); + Sort(fields: SortOptions): Builder { + this.methodCalls.set("Sort", [fields]); return this; } - Limit(limit: number): Builder { - this.request.commands.push({ method: "Limit", args: [limit] }); + Limit(limit: number): Builder { + this.methodCalls.set("Limit", [limit]); return this; } - Offset(offset: number): Builder { - this.request.commands.push({ method: "Offset", args: [offset] }); + Offset(offset: number): Builder { + this.methodCalls.set("Offset", [offset]); return this; } - Delete(): Builder { - this.request.commands.push({ method: "Delete", args: [] }); + Delete(): Builder { + this.methodCalls.set("Delete", []); return this; } - Insert(data: Record): Builder { - this.request.commands.push({ method: "Insert", args: [data] }); + Insert(data: Record): Builder { + this.methodCalls.set("Insert", [data]); return this; } - Set(data: Record): Builder { - this.request.commands.push({ method: "Set", args: [data] }); + Set(data: Record): Builder { + this.methodCalls.set("Set", [data]); return this; } - Update(data: Record): Builder { + Update(data: Record): Builder { this.Set(data); return this; } - OnConflict(...fields: string[]): Builder { - this.request.commands.push({ method: "OnConflict", args: fields }); + OnConflict(...fields: string[]): Builder { + this.methodCalls.set("OnConflict", fields); return this; } - DoUpdate(...fields: string[]): Builder { - this.request.commands.push({ method: "DoUpdate", args: fields }); + DoUpdate(...fields: string[]): Builder { + this.methodCalls.delete("DoNothing"); + this.methodCalls.set("DoUpdate", fields); return this; } - DoNothing(): Builder { - this.request.commands.push({ method: "DoNothing", args: [] }); + DoNothing(): Builder { + this.methodCalls.delete("DoUpdate"); + this.methodCalls.set("DoNothing", []); return this; } - async *Rows() { - this.format(); + async *Rows>(): AsyncGenerator { + 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 any>(template: T): Builder { + this.dtoTemplate = template; + return this; + } + async Query>(): Promise { + const rows = this.Rows(); + const result = []; + for await (const row of rows) { + result.push(row); + } + return result } } \ No newline at end of file diff --git a/dal/Protocol.ts b/dal/Protocol.ts index 96741bb..8eb5c57 100644 --- a/dal/Protocol.ts +++ b/dal/Protocol.ts @@ -55,7 +55,6 @@ export async function *decodeRowsIterator(stream: ReadableStream): A for (;;) { const { value, done } = await reader.read(); if (done) { - console.log("done"); break; } buf = new Uint8Array([...buf, ...value]); diff --git a/dal/__test__/builder.test.ts b/dal/__test__/builder.test.ts new file mode 100644 index 0000000..ff63d5f --- /dev/null +++ b/dal/__test__/builder.test.ts @@ -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(); + 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); +}); diff --git a/dal/__test__/protocol.test.ts b/dal/__test__/protocol.test.ts deleted file mode 100644 index b56e316..0000000 --- a/dal/__test__/protocol.test.ts +++ /dev/null @@ -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); -}); diff --git a/dal/__test__/srv/test.sqlite b/dal/__test__/srv/test.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..8467090a263d61dc6f4d65842e2b6ac870c50599 GIT binary patch literal 8192 zcmeI$F-rq66bJB^Yl^+0M3BSvjS5GXegW?!Nb%HqMrg+zy+Xlya2G_!j((JWA!lcI zcauWbb{77RJo0}b({D?j9*dQ$dD*mU>v&A3Bm`YC6H(uHqAt2-Aux-NBQG@C3h zG=~$uSS@&RZ}g2Wd0I~HvU1M1dYns~RGne6QF4Y&QU-hHDeLz6~ z0uX=z1Rwwb2tWV=5P$##An;EF22#?Qs%%>gdLb>F#>SDPM7gGO@3GiL|6B0!2kM|M A#{d8T literal 0 HcmV?d00001