UNPKG

lendb-client

Version:

(WIP) Browser-client for connecting to [LenDB]{https://github.com/paradis-A/lendb-server}.

744 lines (669 loc) 24.4 kB
import Emittery from "emittery"; import cuid from "cuid"; import Axios, { AxiosInstance } from "axios/dist/axios.min.js"; import cloneDeep from "lodash/cloneDeep"; import isObject from "lodash/isObject"; import Sockette from "sockette"; import Normalize from "./normalize"; import Auth from "./auth"; import { Writable, writable } from "svelte/store"; import pWaitFor from "./pwaitfor"; export default class LenQuery<Type> { protected ref: string; protected listener: iLiveQuery; filters: any = {}; sorts: { [any: string]: "ASC" | "DESC" | null } = {}; skip: number = 0; limit: number = 100; page: number = 0; executing: boolean = false; listening: boolean = false; authenticating: boolean = false; #reactiveData: Writable<Type[] | any[]>; #reactiveCount: Writable<number>; #data: Type[] | any[] = []; #count: number = 0; #initialDataReceived = false timeout = 60000 #subscriptionKey: string; protected aggregates: Aggregate; protected queueBeforeResult: any[] = []; protected operation: string; protected exclusion: string[] = []; protected inclusion: string[] = []; protected emitter: Emittery; protected hook: boolean; protected controller!: AbortController; protected signal!: AbortSignal; protected ws: Sockette; protected compoundFilter: any = {} #ws_key: string; #http: AxiosInstance; private wsUrl: string; constructor(ref: string, http: AxiosInstance, wsUrl: string, emitter: Emittery, auth: Auth) { this.ref = ref; this.#http = http; this.wsUrl = wsUrl; this.emitter = emitter; this.operation = "query"; this.#reactiveData = writable([]); this.#reactiveCount = writable(0); this.#data = []; this.#count = 0; this.emitter.on("login", () => { if (this.listening) { this.cancel(); this.execute(); } }); this.emitter.on("logout", () => { this.#ws_key = null; if (this.listening || this.executing) { this.cancel(); this.unsubscribe(); } }); } get data(): Writable<Type[]> { return this.#reactiveData; } get count(): Writable<number> { return this.#reactiveCount; } like(field: string, value: any, pattern: "both" | "left" | "right") { let val = "*" + value + "*"; if (pattern == "left") val = "*" + value; if (pattern == "right") val = value + "*"; this.filters[field + "[like]"] = val; return this; } notLike(field: string, value: string, pattern: "both" | "left" | "right") { let val = "*" + value + "*"; if (pattern == "left") val = "*" + value; if (pattern == "right") val = value + "*"; this.filters[field + "[!like]"] = val; return this; } gt(field: string, value: any) { this.filters[field + "[>]"] = value; return this; } gte(field: string, value: any) { this.filters[field + "[>=]"] = value; return this; } between(field: string, value: any) { this.filters[field + "[between]"] = value; return this; } notBetween(field: string, value: any) { this.filters[field + "[!between]"] = value; return this; } lt(field: string, value: any) { this.filters[field + "[<]"] = value; return this; } lte(field: string, value: any) { this.filters[field + "[<=]"] = value; return this; } eq(field: string, value: any) { this.filters[field + "[==]"] = value; return this; } notEq(field: string, value: any) { this.filters[field + "[!=]"] = value; return this; } in(field: string, value: any[]) { this.filters[field + "[in]"] = value; return this; } notIn(field: string, value: any[]) { this.filters[field + "[!in]"] = value; return this; } matches(field: string, value: any[]) { this.filters[field + "[matches]"] = value; return this; } notMatches(field: string, value: any[]) { this.filters[field + "[!matches]"] = value; return this; } has(field: string, value: any[]) { this.filters[field + "[has]"] = value; return this; } notHas(field: string, value: any[]) { this.filters[field]["!has"] = value; return this; } contains(field: string, value: any[]) { this.filters[field]["contains"] = value; return this; } notContains(field: string, value: any[]) { this.filters[field]["!contains"] = value; return this; } sort(field: string, asc = false) { this.sorts[field] = asc ? "ASC" : "DESC"; return this; } exclude(fields: string[]) { this.exclusion = fields; return this; } include(fields: string[]) { this.inclusion = fields; return this; } aggregate(groupBy: string, cb: (ops: Aggregate) => void | Aggregate) { this.aggregates = new Aggregate(groupBy); cb(this.aggregates); return this; } compound(cb: (filters:CompoundFilter)=>void){ let tempcf = new CompoundFilter() cb(tempcf) this.compoundFilter = tempcf.filters } protected stripNonQuery(clone: this) { delete clone.emitter; delete clone.wsUrl; delete clone.emitter; delete clone.queueBeforeResult; delete clone.ws; delete clone.signal; delete clone.listening; delete clone.listener; delete clone.emitter; return clone; } on(cb: (event: iLiveQuery) => void) { let events = new iLiveQuery(); cb(events); this.listener = events; } clearFilters() { this.filters = {}; } clearSorts() { this.sorts = {}; } protected toWildCardPath(ref: string) { return ref .split("/") .map((r) => { return cuid.isCuid(r) ? "*" : r; }) .join("/"); } protected createWS(builtQuery: any) { try { let props: any = { subscriptionKey: this.#subscriptionKey, query: builtQuery, }; let ws_auth_canceller = Axios.CancelToken.source(); this.ws = new Sockette(this.wsUrl + "/lenDB", { timeout: 5e3, maxAttempts: Infinity, onopen: () => { if (!this.authenticating) { this.authenticating = true; const ws_authenticator = Object.assign(Axios, this.#http); ws_authenticator .post("lenDB_Auth", JSON.stringify({ type: "authenticate_ws" }), { timeout: Infinity, cancelToken: ws_auth_canceller.token, }) .then((r) => { const res = r.data; this.authenticating = false; if (this.#ws_key && res?.public == true) { this.#ws_key = null; } if (res.key) this.#ws_key = res.key; if (res.public) props.public = res.public; props.key = this.#ws_key; this.ws.send(JSON.stringify(props)); }) .catch((e) => { console.log("Cannot authenticate websocket connection. Retrying."); }); delete props.reconnect; } }, onerror: (e) => { if (!this.authenticating) { props.reconnect = true } else { ws_auth_canceller.cancel(); } }, onreconnect: () => { console.log("Disconnected to the server. Reconnecting."); }, onclose: () => { this.listening = false; }, onmessage: (e) => { let payload: any = e.data; if (typeof e.data == "string") payload = JSON.parse(e.data); if (payload.type == "add") { if (this.listener && this.listener.getEvent("add")) { this.listener.getEvent("add")(payload?.newData); } let tempData = payload?.data if (tempData && Array.isArray(tempData)) { tempData = tempData.map((data) => { return Normalize(data); }); } this.#data = tempData this.#count = payload?.count || payload.count; this.#reactiveCount.set(this.#count); this.#reactiveData.set(this.#data); } if (payload.type == "update") { if (this.listener && this.listener.getEvent("update")) { this.listener.getEvent("update")(payload?.newData); } let tempData = payload?.data if (tempData && Array.isArray(tempData)) { tempData = tempData.map((data) => { return Normalize(data); }); } this.#data = tempData this.#count = payload?.count || payload.count; this.#reactiveCount.set(this.#count); this.#reactiveData.set(this.#data); } if (payload.type == "initialdata") { let tempData = payload?.data if (tempData && Array.isArray(tempData)) { tempData = tempData.map((data) => { return Normalize(data); }); } this.#data = tempData this.#count = payload?.count || payload.count; this.#reactiveCount.set(this.#count); this.#reactiveData.set(this.#data); this.#initialDataReceived = true } if (payload.type == "destroy") { if (this.listener && this.listener.getEvent("destroy")) { this.listener.getEvent("destroy")(payload?.newData); } let tempData = payload?.data if (tempData && Array.isArray(tempData)) { tempData = tempData.map((data) => { return Normalize(data); }); } this.#data = tempData this.#count = payload?.count || payload.count; this.#reactiveCount.set(this.#count); this.#reactiveData.set(this.#data); } }, }); this.listening = true; } catch (error) { console.log(error); } } unsubscribe() { if (this.ws) { this.#subscriptionKey = null; this.listening = false; this.ws.close(1000, "unsubscribe"); } } async execute( options: { page?: number; limit?: number; searchString?: string; live: boolean; timeout?: number } = { live: true } ): Promise<{ data: Type[]; count: number }> { try { if (this.ref.includes("__users__") || this.ref.includes("__tokens__")) { return Promise.reject("Error: cannot access secured refferences use instance.User() instead."); } if (typeof options.live == "undefined") options.live = true; if (this.executing) { this.cancel(); } const { page, limit,timeout } = options; if (page && typeof page == "number") { this.page = page; } if (limit && typeof limit == "number") { this.limit = limit; } if (timeout && typeof timeout == "number") { this.timeout = timeout; } let clone = this.stripNonQuery(cloneDeep(this)); //filter processing if (clone.filters && isObject(clone.filters) && Object.entries(clone.filters).length) { //@ts-ignore clone.filters = this.transformFilters(clone.filters); } else { //@ts-ignore clone.filters = []; } if (clone.compoundFilter && isObject(clone.compoundFilter) && Object.entries(clone.compoundFilter).length) { //@ts-ignore clone.compoundFilter = this.transformFilters(clone.compoundFilter); } else { //@ts-ignore clone.compoundFilter = []; } if (clone.aggregates && clone?.aggregates.list.length) { const { groupBy, list } = clone.aggregates; //@ts-ignore clone.aggregates = { groupBy, list }; } if (clone.sorts && isObject(clone.sorts) && Object.entries(clone.sorts).length) { let tempSorts = []; for (const entry of Object.entries(clone.sorts)) { let key = entry[0]; let value = entry[1]; if (value == "ASC") { tempSorts.push([key, true]); } else if (value == "DESC") { tempSorts.push([key, false]); } } //@ts-ignore clone.sorts = tempSorts; } this.unsubscribe(); this.listening = false; this.executing = true; let res: any = { data: [], count: 0 }; let tempData = []; //@ts-ignore if(clone.aggregates && Object.entries(clone.aggregates).length && clone.compoundFilter.length) throw Error("Error: Cannot aggregate with compundfilter") if (!options.live) { this.ws = null this.controller = new AbortController(); this.signal = this.controller.signal; this.signal.onabort = () => { Promise.reject("Query Cancelled"); }; res = (await this.#http.post("lenDB", JSON.stringify(clone), { signal: this.signal })).data; tempData = res.data if (tempData && Array.isArray(tempData)) { tempData = tempData.map((data) => { return Normalize(data); }); } this.#data = tempData; this.#count = res.count; this.#reactiveData.set(tempData); this.#reactiveCount.set(res?.count); res.data = this.#data this.executing = false return Promise.resolve(res); }else{ //@ts-ignore clone.live = true; this.createWS(clone); await pWaitFor(()=>{ return this.#initialDataReceived },timeout) this.executing = false this.listening = true this.#initialDataReceived = false return Promise.resolve({count: this.#count,data: this.#data}); } } catch (error) { this.executing = false; this.listening = false; if(this.ws)this.ws.close() this.ws = null; return Promise.reject(error); } } cancel() { if (this.controller) { this.controller.abort(); this.executing = false; } return this; } protected transformFilters(clone: this | any){ try { let tempFilters = [] for (const entry of Object.entries(clone.filters)) { let key = entry[0]; let value = entry[1]; if (key.includes("[") || key.includes("]")) { let start = key.indexOf("["); let end = key.indexOf("]"); if (start == -1 || end == -1) { throw new Error("Filter must be enclosed with []"); } let filter = key.substring(start + 1, end); let field = key.substring(0, start); if (operatorBasis.includes(filter)) { if (filter == "in" && !Array.isArray(value)) throw new Error("Invalid filter"); if (filter == "between" && !Array.isArray(value)) throw new Error("Invalid filter"); const alphaOperators = { eq: "==", neq: "!=", gt: ">", gte: ">=", lt: "<", lte: "<=", }; if (filter.startsWith("not")) { let transformedFilter = Object.keys(alphaOperators).includes( filter.substring(2).toLowerCase() ) ? alphaOperators[filter.substring(2).toLowerCase()] : filter.substring(2).toLowerCase(); tempFilters.push([field, transformedFilter, value]); } else { tempFilters.push([field, filter, value]); } } else { throw new Error("Invalid filter"); } } else { if (Array.isArray(value)) { tempFilters.push([key, "in", value]); } else { tempFilters.push([key, "==", value]); } } } return tempFilters } catch (error) { throw Error(error) } } } class Aggregate { list: { field: string; operation: "SUM" | "COUNT" | "MIN" | "MAX" | "AVG"; alias: string; }[] = []; groupBy: string; constructor(groupBy: string) { this.groupBy = groupBy; } sum(field: string, alias: string) { this.list.push({ field, operation: "SUM", alias }); return this; } count(field: string, alias: string) { this.list.push({ field, operation: "COUNT", alias }); return this; } min(field: string, alias: string) { this.list.push({ field, operation: "MIN", alias }); return this; } max(field: string, alias: string) { this.list.push({ field, operation: "MAX", alias }); return this; } avg(field: string, alias: string) { this.list.push({ field, operation: "AVG", alias }); return this; } } class iLiveQuery { callbacks: Function[] = []; protected add: Function = null; protected update: Function = null; protected destroy: Function = null; protected initial: Function = null; onAdd(cb: (e: any, allData: any[]) => void) { this.add = cb; } onInitial(cb: (e: any, allData: any[]) => void) { this.initial = cb; } onUpdate(cb: (e: any, allData: any[]) => void) { this.update = cb; } onDestroy(cb: (e: any, allData: any[]) => void) { this.destroy = cb; } getEvent(event: "add" | "update" | "destroy" | "initial") { if (event == "add") return this.add; if (event == "update") return this.update; if (event == "destroy") return this.destroy; if (event == "initial") return this.initial; } } const operatorBasis = [ "eq", "gt", "gte", "lt", "lte", "like", "in", "neq", "has", "notHas", "contains", "notContains", "notLike", "between", "notIn", "notBetween", "matches", "notEq", "notMatches", "!eq", "!has", "!contains", "!like", "!between", "!in", "!matches", "==", "!=", ">=", "<=", ">", "<", ]; export class CompoundFilter{ filters: any = {}; like(field: string, value: any, pattern: "both" | "left" | "right") { let val = "*" + value + "*"; if (pattern == "left") val = "*" + value; if (pattern == "right") val = value + "*"; this.filters[field + "[like]"] = val; return this; } notLike(field: string, value: string, pattern: "both" | "left" | "right") { let val = "*" + value + "*"; if (pattern == "left") val = "*" + value; if (pattern == "right") val = value + "*"; this.filters[field + "[!like]"] = val; return this; } gt(field: string, value: any) { this.filters[field + "[>]"] = value; return this; } gte(field: string, value: any) { this.filters[field + "[>=]"] = value; return this; } between(field: string, value: any) { this.filters[field + "[between]"] = value; return this; } notBetween(field: string, value: any) { this.filters[field + "[!between]"] = value; return this; } lt(field: string, value: any) { this.filters[field + "[<]"] = value; return this; } lte(field: string, value: any) { this.filters[field + "[<=]"] = value; return this; } eq(field: string, value: any) { this.filters[field + "[==]"] = value; return this; } notEq(field: string, value: any) { this.filters[field + "[!=]"] = value; return this; } in(field: string, value: any[]) { this.filters[field + "[in]"] = value; return this; } notIn(field: string, value: any[]) { this.filters[field + "[!in]"] = value; return this; } matches(field: string, value: any[]) { this.filters[field + "[matches]"] = value; return this; } notMatches(field: string, value: any[]) { this.filters[field + "[!matches]"] = value; return this; } has(field: string, value: any[]) { this.filters[field + "[has]"] = value; return this; } notHas(field: string, value: any[]) { this.filters[field]["!has"] = value; return this; } contains(field: string, value: any[]) { this.filters[field]["contains"] = value; return this; } notContains(field: string, value: any[]) { this.filters[field]["!contains"] = value; return this; } }