[chore] format ts
Signed-off-by: Anton Nesterov <anton@demiurg.io>
This commit is contained in:
parent
0cecd1243f
commit
60d213f21c
15
README.md
15
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")
|
||||
const query = Db.In("users")
|
||||
.Find({
|
||||
fullname: { $glob: "*son" }
|
||||
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" })
|
||||
];
|
||||
```
|
304
dal/Builder.ts
304
dal/Builder.ts
|
@ -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;
|
||||
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;
|
||||
}
|
||||
private formatRequest(): void {
|
||||
this.request.commands = []
|
||||
METHODS.forEach((method) => {
|
||||
const args = this.methodCalls.get(method);
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
this.request.commands.push({ method, args });
|
||||
})
|
||||
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];
|
||||
}
|
||||
}
|
||||
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 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
@ -1 +1 @@
|
|||
export { default as DAL } from './Builder';
|
||||
export { default as DAL } from "./Builder";
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# [wip] DAL
|
||||
NodeJS Client for the [DAL]() Server.
|
||||
|
||||
NodeJS Client for the [DAL]() Server.
|
||||
|
|
|
@ -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 ."
|
||||
}
|
||||
}
|
|
@ -2,17 +2,19 @@
|
|||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue