UNPKG

lendb-server

Version:

`LenDB Server` is a wrapper around another database called Acebase that acts like a client. Think of it as parse-server and firebase had baby then voilah!!! `Hello World!!` LenDB is born.

427 lines (386 loc) 13.8 kB
import Emittery from "emittery"; import cuid from "cuid"; import { cloneDeep, isObject } from "lodash"; import { Serializer } from "./"; import Normalize from "./normalize"; import { AceBase } from "acebase"; import { DataReferenceQuery } from "acebase-core"; export default class LenQuery { protected ref: string; filters: any = {}; sorts: { [any: string]: "ASC" | "DESC" | null } = {}; skip: number = 0; limit: number = 100; page: number = 0; listener: iLiveQuery; #live = false; #liveRef: DataReferenceQuery; #acebase: AceBase; protected aggregates: Aggregate; protected operation: string; protected exclusion: string[] = []; protected inclusion: string[] = []; searchString: string; protected serializer: Serializer; protected emitter: Emittery; protected unsubscribePrevious: Function = null; protected hook: boolean; constructor(ref: string, emitter: Emittery, serializer: Serializer, acebase: AceBase) { this.serializer = serializer; this.emitter = emitter; this.ref = ref; this.operation = "query"; this.hook = false; this.#acebase = acebase; } 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; } include(fields: string[]) { this.inclusion = fields; } search(word: string) { this.searchString = word; return this; } on(cb: (event: iLiveQuery) => void) { let events = new iLiveQuery(); cb(events); this.listener = events; this.#live = true; } protected stripNonQuery(clone: this) { delete clone.serializer; delete clone.emitter; delete clone.unsubscribePrevious; delete clone.listener; return clone; } protected toWildCardPath(ref: string) { return ref .split("/") .map((r) => { return cuid.isCuid(r) ? "*" : r; }) .join("/"); } aggregate(groupBy: string, cb: (ops: Aggregate) => void | Aggregate) { this.aggregates = new Aggregate(groupBy); cb(this.aggregates); return this; } async execute( options: { page?: number; limit?: number; hook?: boolean } = { hook: false, } ): Promise<{ data: any[]; count: number }> { try { if (this.ref.includes("__users__") || this.ref.includes("__tokens__")) { return Promise.reject("Error: cannot access secured refferences use instance.User() instead."); } const { page, limit, hook } = options; this.hook = hook; let clone = this.stripNonQuery(cloneDeep(this)); //clear white spaces ons earch string if (clone.searchString) { let noWhiteSpace = clone.searchString.split(" "); if (noWhiteSpace.every((v) => v == "")) { delete clone.searchString; } if (!clone.searchString.length) { delete clone.searchString; } } if (clone.filters && isObject(clone.filters) && Object.entries(clone.filters).length) { 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]); } } } //@ts-ignore clone.filters = tempFilters; } else { //@ts-ignore clone.filters = []; } 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; } if (page && typeof page == "number") clone.page = page; if (limit && typeof limit == "number") clone.limit = limit; if (this.#live && this.listener.callbacks.length) { await this.createListener(clone); } else { let res = await this.serializer.Execute(clone); let tempData = res?.data; if (tempData && Array.isArray(tempData)) { tempData = tempData.map((data) => { return Normalize(data); }); } res.data = tempData; return Promise.resolve(res); } } catch (error) { return Promise.reject(error); } } protected async createListener(transaction) { try { this.unsubscribe() this.#liveRef = this.serializer.applyFilters(transaction, this.#acebase.query(transaction.ref)); this.#liveRef.on("add", (rqe) => { this.serializer.LivePayload(transaction, rqe).then((result) => { this.listener.getEvent("add")(result); }); }); this.#liveRef.on("change", (rqe) => { this.serializer.LivePayload(transaction, rqe).then((result) => { this.listener.getEvent("update")(result); }); }); this.#liveRef.on("remove", (rqe) => { this.serializer.LivePayload(transaction, rqe).then((result) => { this.listener.getEvent("destroy")(result); }); }); await this.#liveRef.find(); let res = await this.serializer.Execute(transaction); //turns off when execute called again and if on() not called before execute this function will not be executed this.#live = false; return Promise.resolve({ data: res.data, cout: res.count }); } catch (error) { return Promise.reject(error); } } unsubscribe() { if(this.#liveRef){ this.#liveRef.off("add",()=>{}) this.#liveRef.off("change",()=>{}) this.#liveRef.off("remove",()=>{}) this.#liveRef = null } } } 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; onAdd(cb: (e: any) => void) { this.add = cb; } onUpdate(cb: (e: any) => void) { this.update = cb; } onDestroy(cb: (e: any) => void) { this.destroy = cb; } getEvent(event: "add" | "update" | "destroy") { if (event == "add") return this.add; if (event == "update") return this.update; if (event == "destroy") return this.update; } } 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", "==", "!=", ">=", "<=", ">", "<", ];