feat(jslib): ✨ add basic query builder
parent
bb8d390d9e
commit
3526a164fa
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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))');
|
||||
});
|
||||
*/
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue