[chore] format ts

Signed-off-by: Anton Nesterov <anton@demiurg.io>
This commit is contained in:
Anton Nesterov 2024-08-15 15:04:44 +02:00
parent 0cecd1243f
commit 60d213f21c
No known key found for this signature in database
GPG key ID: 59121E8AE2851FB5
10 changed files with 282 additions and 280 deletions

View file

@ -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" }
]
```
console.log(users)[
({ id: 25, fullname: "John Menson" }, { id: 76, fullname: "John Johnson" })
];
```

BIN
bun.lockb

Binary file not shown.

View file

@ -4,22 +4,22 @@ import { METHODS, encodeRequest, decodeRowsIterator } from "./Protocol";
type Primitive = string | number | boolean | null;
interface Filter extends Record<string, unknown> {
$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<string, 1 | -1 | "asc" | "desc">;
type Options = {
database: string;
url: string;
database: string;
url: string;
};
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,
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<I> {
this.methodCalls.set("In", [table]);
return this;
}
Find(filter: FindFilter): Builder<I> {
this.methodCalls.set("Find", [filter]);
return this;
}
Select(fields: string[]): Builder<I> {
this.methodCalls.set("Select", fields);
return this;
}
Fields(fields: string[]): Builder<I> {
this.Select(fields);
return this;
}
Join(...joins: JoinFilter[]): Builder<I> {
this.methodCalls.set("Join", joins);
return this;
}
Group(fields: string[]): Builder<I> {
this.methodCalls.set("Group", fields);
return this;
}
Sort(fields: SortOptions): Builder<I> {
this.methodCalls.set("Sort", [fields]);
return this;
}
Limit(limit: number): Builder<I> {
this.methodCalls.set("Limit", [limit]);
return this;
}
Offset(offset: number): Builder<I> {
this.methodCalls.set("Offset", [offset]);
return this;
}
Delete(): Builder<I> {
this.methodCalls.set("Delete", []);
return this;
}
Insert(data: Record<string, unknown>): Builder<I> {
this.methodCalls.set("Insert", [data]);
return this;
}
Set(data: Record<string, unknown>): Builder<I> {
this.methodCalls.set("Set", [data]);
return this;
}
Update(data: Record<string, unknown>): Builder<I> {
this.Set(data);
return this;
}
OnConflict(...fields: string[]): Builder<I> {
this.methodCalls.set("OnConflict", fields);
return this;
}
DoUpdate(...fields: string[]): Builder<I> {
this.methodCalls.delete("DoNothing");
this.methodCalls.set("DoUpdate", fields);
return this;
}
DoNothing(): Builder<I> {
this.methodCalls.delete("DoUpdate");
this.methodCalls.set("DoNothing", []);
return this;
}
async *Rows<T = InstanceType<I>>(): AsyncGenerator<T> {
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<string, unknown[]> = 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<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);
}
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<I> {
this.methodCalls.set("Find", [filter]);
return this;
}
Select(fields: string[]): Builder<I> {
this.methodCalls.set("Select", fields);
return this;
}
Fields(fields: string[]): Builder<I> {
this.Select(fields);
return this;
}
Join(...joins: JoinFilter[]): Builder<I> {
this.methodCalls.set("Join", joins);
return this;
}
Group(fields: string[]): Builder<I> {
this.methodCalls.set("Group", fields);
return this;
}
Sort(fields: SortOptions): Builder<I> {
this.methodCalls.set("Sort", [fields]);
return this;
}
Limit(limit: number): Builder<I> {
this.methodCalls.set("Limit", [limit]);
return this;
}
Offset(offset: number): Builder<I> {
this.methodCalls.set("Offset", [offset]);
return this;
}
Delete(): Builder<I> {
this.methodCalls.set("Delete", []);
return this;
}
Insert(data: Record<string, unknown>): Builder<I> {
this.methodCalls.set("Insert", [data]);
return this;
}
Set(data: Record<string, unknown>): Builder<I> {
this.methodCalls.set("Set", [data]);
return this;
}
Update(data: Record<string, unknown>): Builder<I> {
this.Set(data);
return this;
}
OnConflict(...fields: string[]): Builder<I> {
this.methodCalls.set("OnConflict", fields);
return this;
}
DoUpdate(...fields: string[]): Builder<I> {
this.methodCalls.delete("DoNothing");
this.methodCalls.set("DoUpdate", fields);
return this;
}
DoNothing(): Builder<I> {
this.methodCalls.delete("DoUpdate");
this.methodCalls.set("DoNothing", []);
return this;
}
async *Rows<T = InstanceType<I>>(): AsyncGenerator<T> {
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<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
}
}
return result;
}
}

View file

@ -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<Uint8Array>): AsyncGenerator<Row> {
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<Uint8Array>,
): AsyncGenerator<Row> {
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;
}
}
}

View file

@ -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<any[]>();
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<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("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);
});

Binary file not shown.

View file

@ -1 +1 @@
export { default as DAL } from './Builder';
export { default as DAL } from "./Builder";

View file

@ -1,3 +1,3 @@
# [wip] DAL
NodeJS Client for the [DAL]() Server.
NodeJS Client for the [DAL]() Server.

View file

@ -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 ."
}
}
}

View file

@ -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);
Deno.writeFileSync("proto_test.msgpack", encoded);