feat(jslib): add basic query builder

feat/lang-upgrades-keys
Tomáš Mládek 2023-10-09 21:53:44 +02:00
parent bb8d390d9e
commit 3526a164fa
5 changed files with 104 additions and 48 deletions

View File

@ -1,5 +1,5 @@
import LRU from "lru-cache";
import type { UpObject } from ".";
import type { Query, UpObject } from ".";
import { UpListing } from ".";
import type {
ADDRESS_TYPE,
@ -59,29 +59,31 @@ export class UpEndApi {
return listing.entries[0];
}
public async query(query: string): Promise<UpListing> {
const cacheResult = this.queryOnceLRU.get(query);
public async query(query: string | Query): Promise<UpListing> {
const queryStr = query.toString();
const cacheResult = this.queryOnceLRU.get(queryStr);
if (!cacheResult) {
if (!this.inFlightRequests[query]) {
if (!this.inFlightRequests[queryStr]) {
dbg(`Querying: ${query}`);
this.inFlightRequests[query] = new Promise((resolve, reject) => {
this.inFlightRequests[queryStr] = new Promise((resolve, reject) => {
fetch(`${this.apiUrl}/query`, {
method: "POST",
body: query,
body: queryStr,
keepalive: true,
})
.then(async (response) => {
resolve(new UpListing(await response.json()));
this.inFlightRequests[query] = null;
this.inFlightRequests[queryStr] = null;
})
.catch((err) => reject(err));
});
} else {
dbg(`Chaining request for ${query}...`);
dbg(`Chaining request for ${queryStr}...`);
}
return await (this.inFlightRequests[query] as Promise<UpListing>); // TODO?
return await (this.inFlightRequests[queryStr] as Promise<UpListing>); // TODO?
} else {
dbg(`Returning cached: ${query}`);
dbg(`Returning cached: ${queryStr}`);
return cacheResult;
}
}

View File

@ -1,5 +1,6 @@
import type { IEntry, IValue, ListingResult } from "./types";
export { UpEndApi } from "./api";
export { Query } from "./query";
export class UpListing {
public readonly entries: UpEntry[];

54
tools/upend_js/query.ts Normal file
View File

@ -0,0 +1,54 @@
import type { Address } from "./types";
import { isAddress } from "./types";
type OneOrMany<T> = T | T[];
export class Query {
private _query: string | undefined;
public matches(
entity: OneOrMany<string> | undefined,
attribute: OneOrMany<string> | undefined,
value: OneOrMany<string | number | Address> | undefined
): Query {
const query = new Query();
let entityStr;
if (entity === undefined) {
entityStr = "?";
} else {
entityStr = Array.isArray(entity) ? `(in ${entity.join(" ")})` : entity;
}
let attributeStr;
if (attribute === undefined) {
attributeStr = "?";
} else {
attributeStr = Array.isArray(attribute)
? `(in ${attribute.map((a) => `"${a}"`).join(" ")})`
: `"${attribute}"`;
}
let valueStr;
if (value === undefined) {
valueStr = "?";
} else {
valueStr = (Array.isArray(value) ? value : [value])
.map((v) => {
if (typeof v === "number") return v;
if (isAddress(v)) return v;
if (typeof v === "string") return `"${v}"`;
})
.join(" ");
valueStr = Array.isArray(value) ? `(in ${valueStr})` : valueStr;
}
query._query = `(matches ${entityStr} ${attributeStr} ${valueStr})`;
return query;
}
public toString(): string {
if (!this._query) throw new Error("Query is not defined");
return this._query;
}
}

View File

@ -1,41 +1,36 @@
/*
import test from "ava";
import http from "http";
import { UpObject, UpEntry, UpListing } from ".";
import type { Address, IEntry, ListingResult } from "./types";
import { Query } from "./query";
function fetchJSON(url: string): Promise<unknown> {
return new Promise((resolve, reject) => {
http
.get(url, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
resolve(JSON.parse(data));
});
})
.on("error", (error) => {
reject(error);
});
});
}
test("basic hier listing", async (t) => {
const data = (await fetchJSON("http://localhost:8093/api/hier/NATIVE")) as {
target: Address;
entries: IEntry[];
};
const native = new UpObject(data.target);
// console.log(native.asDict(data.entries));
// console.log(asDict(Object.values(data)));
t.pass();
test("query matches simple", (t) => {
const query = new Query().matches("entity", "attribute", "value");
t.is(query.toString(), '(matches entity "attribute" "value")');
});
test("query matches anything", (t) => {
const query = new Query().matches(undefined, undefined, undefined);
t.is(query.toString(), "(matches ? ? ?)");
});
test("query matches array", (t) => {
const query = new Query().matches("entity", "attribute", [
"value1",
"value2",
]);
t.is(query.toString(), '(matches entity "attribute" (in "value1" "value2"))');
});
test("query matches addresses", (t) => {
const query = new Query().matches("entity", "attribute", [
"@address1",
"@address2",
]);
t.is(
query.toString(),
'(matches entity "attribute" (in @address1 @address2))'
);
});
test("query matches numbers", (t) => {
const query = new Query().matches("entity", "attribute", [1, 2]);
t.is(query.toString(), '(matches entity "attribute" (in 1 2))');
});
*/

View File

@ -2,6 +2,10 @@ export type Address = string;
export type ADDRESS_TYPE = "Hash" | "Uuid" | "Attribute" | "Url";
export type VALUE_TYPE = "Address" | "String" | "Number" | "Invalid";
export function isAddress(address: string): address is Address {
return address.startsWith("@");
}
/**
* A single atomic entry in UpEnd.
*/