diff --git a/README.md b/README.md index d262403..bf07984 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,14 @@ Data Accees Layer for SQL databases written in Go. Mongodb inspired query interface: ```typescript -const query = Db - .In("users") - .Find({ - fullname: { $glob: "*son" } +const query = Db.In("users") + .Find({ + fullname: { $glob: "*son" }, }) - .Query() + .Query(); // Result: -console.log(users) -[ - { id: 25, fullname: "John Menson" }, - { id: 76, fullname: "John Johnson" } -] -``` \ No newline at end of file +console.log(users)[ + ({ id: 25, fullname: "John Menson" }, { id: 76, fullname: "John Johnson" }) +]; +``` diff --git a/bun.lockb b/bun.lockb index 35721ef..f881ad2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/dal/Builder.ts b/dal/Builder.ts index 1a096e7..b7e0d7a 100644 --- a/dal/Builder.ts +++ b/dal/Builder.ts @@ -4,22 +4,22 @@ import { METHODS, encodeRequest, 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]; + $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; + [key: string]: Primitive | Filter | Filter[] | undefined; } type JoinCondition = "inner" | "left" | "cross" | "full outer"; @@ -32,150 +32,146 @@ type JoinFilter = { type SortOptions = Record; type Options = { - database: string; - url: string; + database: string; + url: string; }; +export default class Builder 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, + db: opts.database, + commands: [], + }; + this.url = opts.url; + } + private formatRequest(): void { + this.request.commands = []; + METHODS.forEach((method) => { + const args = this.methodCalls.get(method); + if (!args) { + return; + } + 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 { + this.methodCalls.set("In", [table]); + return this; + } + Find(filter: FindFilter): Builder { + this.methodCalls.set("Find", [filter]); + return this; + } + Select(fields: string[]): Builder { + this.methodCalls.set("Select", fields); + return this; + } + Fields(fields: string[]): 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; + } + async *Rows>(): AsyncGenerator { + this.formatRequest(); + const response = await fetch(this.url, { + method: "POST", + body: new Blob([encodeRequest(this.request)]), + headers: { + "Content-Type": "application/x-msgpack", + }, + }); + if (response.status !== 200) { + throw new Error(await response.text()); + } -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, - db: opts.database, - commands: [], - }; - this.url = opts.url; + 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); } - private formatRequest(): void { - this.request.commands = [] - METHODS.forEach((method) => { - const args = this.methodCalls.get(method); - if (!args) { - return; - } - this.request.commands.push({ method, args }); - }) + } + 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); } - 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.methodCalls.set("Find", [filter]); - return this; - } - Select(fields: string[]): Builder { - this.methodCalls.set("Select", fields); - return this; - } - Fields(fields: string[]): 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; - } - async *Rows>(): AsyncGenerator { - this.formatRequest(); - const response = await fetch(this.url, { - method: "POST", - body: new Blob([encodeRequest(this.request)]), - headers: { - "Content-Type": "application/x-msgpack", - }, - }); - if (response.status !== 200) { - throw new Error(await response.text()); - } - - 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); - } - } - 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 + return result; + } +} diff --git a/dal/Protocol.ts b/dal/Protocol.ts index 8eb5c57..81b39b8 100644 --- a/dal/Protocol.ts +++ b/dal/Protocol.ts @@ -1,68 +1,73 @@ -import { encode, decode } from '@msgpack/msgpack'; +import { encode, decode } from "@msgpack/msgpack"; export interface Method { - method: string; - args: any; + method: string; + args: any; } export interface Request { - id: number; - db: string; - commands: Method[]; + id: number; + db: string; + commands: Method[]; } -export const METHODS = "In|Find|Select|Fields|Join|Group|Sort|Limit|Offset|Delete|Insert|Set|Update|OnConflict|DoUpdate|DoNothing".split("|"); +export const METHODS = + "In|Find|Select|Fields|Join|Group|Sort|Limit|Offset|Delete|Insert|Set|Update|OnConflict|DoUpdate|DoNothing".split( + "|", + ); export function encodeRequest(request: Request): Uint8Array { - return encode(request); + return encode(request); } export interface Row { - r: unknown[]; + r: unknown[]; } const ROW_TAG = [0x81, 0xa1, 0x72]; export function decodeRows(input: Uint8Array): Row[] { - const rows = []; - let count = 0; - let buf = []; - while (count < input.length) { - if (input.at(count) != 0x81) { - buf.push(input.at(count)); - count++; - continue - } - const [a, b, c] = ROW_TAG; - const [aa, bb, cc] = input.slice(count, count + 4); - if (aa == a && bb == b && cc == c) { - rows.push([...ROW_TAG, ...buf]); - buf = []; - count += 3; - } else { - buf.push(input.at(count)); - count++; - } + const rows = []; + let count = 0; + let buf = []; + while (count < input.length) { + if (input.at(count) != 0x81) { + buf.push(input.at(count)); + count++; + continue; } - rows.push([...ROW_TAG, ...buf]); - rows.shift(); - return rows.map((row) => decode(new Uint8Array(row as number[]))) as Row[]; + const [a, b, c] = ROW_TAG; + const [aa, bb, cc] = input.slice(count, count + 4); + if (aa == a && bb == b && cc == c) { + rows.push([...ROW_TAG, ...buf]); + buf = []; + count += 3; + } else { + buf.push(input.at(count)); + count++; + } + } + rows.push([...ROW_TAG, ...buf]); + rows.shift(); + return rows.map((row) => decode(new Uint8Array(row as number[]))) as Row[]; } -export async function *decodeRowsIterator(stream: ReadableStream): AsyncGenerator { - const reader = stream.getReader(); - let buf = new Uint8Array(); - for (;;) { - const { value, done } = await reader.read(); - if (done) { - break; - } - buf = new Uint8Array([...buf, ...value]); - // the server flushes after each row - // so we decode "complete" rows - const rows = decodeRows(buf); - for (const row of rows) { - yield row; - } +export async function* decodeRowsIterator( + stream: ReadableStream, +): AsyncGenerator { + const reader = stream.getReader(); + let buf = new Uint8Array(); + for (;;) { + const { value, done } = await reader.read(); + if (done) { + break; } -} \ No newline at end of file + buf = new Uint8Array([...buf, ...value]); + // the server flushes after each row + // so we decode "complete" rows + const rows = decodeRows(buf); + for (const row of rows) { + yield row; + } + } +} diff --git a/dal/__test__/builder.test.ts b/dal/__test__/builder.test.ts index ff63d5f..e1c4e68 100644 --- a/dal/__test__/builder.test.ts +++ b/dal/__test__/builder.test.ts @@ -1,61 +1,61 @@ import { test, expect } from "bun:test"; -import { DAL } from ".." +import { DAL } from ".."; const options = { - database: "test.sqlite", - url: "http://localhost:8111", -} + database: "test.sqlite", + url: "http://localhost:8111", +}; class DTO { - id: number = 0; - name: string = ""; - data: string = ""; - age: number | undefined; + 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, 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("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); +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__/srv/test.sqlite b/dal/__test__/srv/test.sqlite deleted file mode 100644 index 8467090..0000000 Binary files a/dal/__test__/srv/test.sqlite and /dev/null differ diff --git a/dal/index.ts b/dal/index.ts index 6486bff..5fcba6a 100644 --- a/dal/index.ts +++ b/dal/index.ts @@ -1 +1 @@ -export { default as DAL } from './Builder'; \ No newline at end of file +export { default as DAL } from "./Builder"; diff --git a/dal/readme.md b/dal/readme.md index 5dc6253..d44497a 100644 --- a/dal/readme.md +++ b/dal/readme.md @@ -1,3 +1,3 @@ # [wip] DAL -NodeJS Client for the [DAL]() Server. +NodeJS Client for the [DAL]() Server. diff --git a/package.json b/package.json index 86e7953..146a99a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "module": "dal/index.ts", "type": "module", "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "prettier": "^3.3.3" }, "peerDependencies": { "typescript": "^5.0.0" @@ -13,7 +14,8 @@ }, "scripts": { "test:client": "bun test:*", - "test:dal" : "bun test dal/__test__", - "test:serve": "cd dal/__test__/srv && go run main.go" + "test:dal": "bun test dal/__test__", + "test:serve": "cd dal/__test__/srv && go run main.go", + "fmt": "prettier --write ." } -} \ No newline at end of file +} diff --git a/pkg/__test__/proto_test.ts b/pkg/__test__/proto_test.ts index a7dd59c..671d4d1 100644 --- a/pkg/__test__/proto_test.ts +++ b/pkg/__test__/proto_test.ts @@ -2,21 +2,23 @@ import { encode } from "https://deno.land/x/msgpack@v1.2/mod.ts"; const Query = { - "db": "database.sqlite", - "commands": [ - {"method": "In", "args": ["data"]}, + db: "database.sqlite", + commands: [ + { method: "In", args: ["data"] }, { - "method": "Find", - "args": [{ - "a": 1, - "b": { - "$gt": 2, - }, - }] + method: "Find", + args: [ + { + a: 1, + b: { + $gt: 2, + }, + }, + ], }, ], }; const encoded: Uint8Array = encode(Query); //@ts-ignore -Deno.writeFileSync("proto_test.msgpack", encoded); \ No newline at end of file +Deno.writeFileSync("proto_test.msgpack", encoded);