UNPKG

generalised-datastore

Version:

Generalised Datastore (GDS) — a decentralized data engine and RPC layer built for Web 4.0 applications, enabling distributed joins, caching, and remote program execution across nodes.

484 lines (413 loc) 14.4 kB
import { hash } from "godprotocol/utils/hash.js"; import Events from "./Events.js"; import { _id } from "./utils/functions.js"; class Queries extends Events { constructor() { super(); } write_file = async (path, data, options = {}) => { let folder = this.folder.ds ? this.folder : this; if (folder.account) { let { write_bulks, address } = options; options.address = address || folder.address; options.account = folder.account; if (write_bulks) { write_bulks = write_bulks.split("-"); let ds = folder.ds; if (write_bulks.length === 1) { let bulk = ds.bulks[write_bulks[0]]; if (!bulk) { bulk = {}; ds.bulks[write_bulks[0]] = bulk; } bulk[path] = { path, content: data, options }; } else { let contents = ds.bulks[write_bulks[1]]; delete ds.bulks[write_bulks]; contents[path] = { content: data, path, options }; await folder.ds.manager.oracle.write(null, Object.values(contents)); // for (let p in contents){ // await folder.ds.manager.oracle.write(contents[p].path, contents[p].content) // } } } else await folder.ds.manager.oracle.write(path, data, { account: folder.account, address: address || folder.address, }); } else { await folder.ds.set_caches(path, data); } }; read_file = async (path, options = {}) => { let { when } = options; if (!path.includes("/")) path = `${this.path}/${path}`; if (when) path = `${path}/${when}`; let cont, folder = this.folder.ds ? this.folder : this; if (folder.account) { cont = await folder.ds.manager.oracle.read(path, { account: folder.account, address: options.address || folder.address, }); } else { cont = await folder.ds.get_cache(path); } try { cont = cont && JSON.parse(cont); } catch (e) {} return cont; }; _read_data = async (query, options = {}) => { let { excludes, skip, limit, cursor, no_retrieve, or, one, rm, no_joins } = options; no_joins = rm || no_joins; excludes = excludes || new Array(); limit = one ? 1 : Number(limit) || -1; skip = Number(skip) || -1; let id = cursor || this.config.config.recent_file; let matches = new Array(), ids = new Array(); while (id && limit) { let pth = `${this.path}/${id}`, content = await this.read_file(pth); if (content.deleted) break; if (excludes.includes(id)) { id = content.previous; continue; } if (!no_retrieve && this.config.config.is_index.length) { let parent_addr = this.address.split("/").slice(0, -1).join("/"); let content_folder = await this.ds.folder(`${parent_addr}`, { account: this.account, }); content = await content_folder.read_data( { _id: content.content._id }, { one: true, rm: true } ); } if (await this.pass(content.content, query, or)) { if (skip <= 0) { if (limit > 0) limit--; matches.push(rm ? { ...content } : content.content); } else skip--; } ids.push(id); id = content.previous; if (ids.includes(id)) break; } !no_joins && (await this.read_joins(matches)); return one ? matches[0] : matches; }; read_joins = async (data) => { return data; }; read_data = async (query, options = {}) => { let data = await this._read_data(query, options); return data; }; array_comparison = (arr, comparison) => { let find = false; for (let a = 0; a < arr.length; a++) { if (arr[a] === comparison) { find = true; break; } } return find; }; pass = async (content, query, or = false) => { let pass = new Array(); if (query == null) return true; for (let q in query) { let qval = query[q], lval = content[q]; if (qval === undefined) continue; if (Array.isArray(lval) || Array.isArray(qval)) { let arr1, arr2; if (!Array.isArray(lval)) { arr1 = new Array(lval); arr2 = qval; } else if (!Array.isArray(qval)) { arr1 = new Array(qval); arr2 = lval; } else (arr1 = qval), (arr2 = lval); let m = false; for (let l = 0; l < arr1.length; l++) if (this.array_comparison(arr2, arr1[l])) { m = true; break; } pass.push(m); } else if (typeof qval === "object") { if (Object(qval).hasOwnProperty("$ne")) pass.push(lval !== qval["$ne"]); else if (Object(qval).hasOwnProperty("$e")) pass.push(lval === qval["$e"]); else if (Object(qval).hasOwnProperty("$gt")) pass.push(lval > qval["$gt"]); else if (Object(qval).hasOwnProperty("$lt")) pass.push(lval < qval["$lt"]); else if (Object(qval).hasOwnProperty("$gte")) pass.push(lval >= qval["$gte"]); else if (Object(qval).hasOwnProperty("$lte")) pass.push(lval <= qval["$lte"]); else if (Object(qval).hasOwnProperty("$includes")) pass.push(lval.includes && lval.includes(qval["$includes"])); } else pass.push(lval === qval); } if (or) return !!pass.find((p) => p); else { for (let p = 0; p < pass.length; p++) if (!pass[p]) return false; return true; } }; operators = ["ne", "e", "lte", "gte", "lt", "gt", "includes"]; update_next = async (file, _id, prev = false, write_bulks = null) => { if (!file) return; let addr = prev === "future" ? file : `${this.address}/${file}`; addr = `${addr}`; let content = await this.read_file(addr); if (prev === "future") { content.future = _id; } else if (_id === "deleted") { content.deleted = true; } else content[prev ? "previous" : "next"] = _id; await this.write_file(addr, JSON.stringify(content), { write_bulks }); }; write_join_object = async ({ address, value, options }) => { let folder = await this.ds.folder(address, { account: this.account }); let res = await folder.write(value, { write_bulks: options.write_bulks, replace: options.replace, }); return res; }; handle_joins = async (data, options) => { for (let j in this.config.config.joins) { let joi = this.config.config.joins[j], address; let value = data[j]; if (typeof joi === "string") { address = joi; } else address = joi.address; if (!address) continue; if (value && typeof value === "object") { if (Array.isArray(value)) { for (let v = 0; v < value.length; v++) { let item = value[v]; let res = await this.write_join_object({ address, value: item, options, }); data[j][v] = res._id; } } else { let res = await this.write_join_object({ address, value, options }); data[j] = res._id; } } } }; _write_data = async (data, options = {}) => { let { replace, update, persist, write_bulks } = options; let timestamp; data = { ...data }; if (!data.created) { data.created = Date.now(); } data.updated = Date.now(); timestamp = data.updated; if (!data._id) data._id = _id(); write_bulks = write_bulks || Math.random().toString(); await this.handle_joins(data, { ...options, write_bulks }); let datapath = `${this.path}/${data._id}`; let content = await this.read_file(datapath); let exists = content && !content.deleted, previous = this.config.config.recent_file; if (!exists) { await this.write_file( datapath, JSON.stringify({ content: data, previous }), { write_bulks } ); if (previous) { await this.update_next(previous, data._id, null, write_bulks); } } else if (replace) { let prev_content = JSON.stringify(content); if (this.config.config.recent_file !== data._id && !update) { if (content.previous) await this.update_next( content.previous, content.next, null, write_bulks ); if (content.next) await this.update_next( content.next, content.previous, true, write_bulks ); content.previous = this.config.config.recent_file; await this.update_next( this.config.config.recent_file, data._id, null, write_bulks ); this.config.config.recent_file = data._id; } content.past = await this.hash_content(content); content.content = data; if (this.config.config.blocks) { prev_content = JSON.parse(prev_content); await this.write_file( `${datapath}.blocks/${content.past}`, JSON.stringify(prev_content), { write_bulks } ); if (prev_content.past) { await this.update_next( `${datapath}.blocks/${prev_content.past}`, content.past, "future", write_bulks ); } } await this.write_file(datapath, JSON.stringify(content), { write_bulks }); } !update && (!exists || replace) && (await this.config.write( data, options.write_bulks || `write-${write_bulks}` )); let res = { _id: data._id, created: data.created, updated: data.updated, existed: exists || false, replaced: replace || false, address: this.address, timestamp, datapath, }; return res; }; hash_content = async (content) => { let data = { ...content.data }; delete data.updated; return hash(JSON.stringify(data).concat(content.past)); }; write_data = async (data, options = {}) => { let res = await this._write_data(data, options); return res; }; update_snips = async (entry, update_query) => { let prior_values = new Object(); for (let prop in update_query) { let update_value = update_query[prop]; prior_values[prop] = entry[prop]; if (typeof update_value === "object") { if (Object(update_value).hasOwnProperty("$inc")) { if ( typeof entry[prop] === "number" && typeof update_value["$inc"] === "number" ) entry[prop] += update_value["$inc"]; else if ( typeof entry[prop] !== "number" && typeof update_value["$inc"] === "number" ) entry[prop] = update_value["$inc"]; } else if (Object(update_value).hasOwnProperty("$dec")) { if ( typeof entry[prop] === "number" && typeof update_value["$dec"] === "number" ) entry[prop] -= update_value["$dec"]; else if ( typeof entry[prop] !== "number" && typeof update_value["$dec"] === "number" ) entry[prop] = update_value["$dec"] * -1; } else if (Object(update_value).hasOwnProperty("$set")) { let value = update_value["$set"]; if (!Array.isArray(value)) value = new Array(value); if (Array.isArray(entry[prop])) value.map( (val) => !entry[prop].includes(val) && entry[prop].push(val) ); else if (value.includes(entry[prop])) entry[prop] = new Array(...value); else entry[prop] = new Array(entry[prop], ...value); } else if ( Object(update_value).hasOwnProperty("$unshift") || Object(update_value).hasOwnProperty("$push") ) { let value = update_value["$push"] || update_value["$unshift"]; if (!Array.isArray(value)) value = new Array(value); if (Array.isArray(entry[prop])) entry[prop][ Object(update_value).hasOwnProperty("$push") ? "push" : "unshift" ](...value); else { let init_line_prop = entry[prop]; entry[prop] = new Array(...value); if (!init_line_prop) entry[prop].unshift(init_line_prop); } } else if (Object(update_value).hasOwnProperty("$splice")) { let value = update_value["$splice"], line_value = entry[prop]; if (!Array.isArray(value)) value = new Array(value); if (Array.isArray(line_value)) { entry[prop] = line_value.filter((val) => !value.includes(val)); } else if (!Array.isArray(line_value) && line_value === value) delete entry[prop]; } else entry[prop] = update_value; } else entry[prop] = update_value; } return prior_values; }; update_data = async (query, update_query, options = {}) => { let data = await this._read_data(query, options); if (data && !Array.isArray(data)) data = [data]; for (let d = 0; d < data.length; d++) { let datum = data[d]; let bfr = JSON.stringify(datum); let prior_values = await this.update_snips(datum, update_query); await this.config.update( bfr, datum, prior_values, options.write_bulks || "update" ); await this._write_data(datum, { replace: true, update: true }); } return data; // await this.respond(data, {method: 'update'}) }; remove_joins = async (datum) => {}; remove_data = async (query, options) => { let data = await this._read_data(query, { ...options, rm: true, no_retrieve: !!this.config.config.is_index.length, }); if (data && !Array.isArray(data)) data = [data]; let datum; while (data.length) { datum = data.pop(); await this.update_next(datum.content._id, "deleted"); if (datum.previous) await this.update_next(datum.previous, datum.next); if (datum.next) await this.update_next(datum.next, datum.previous, true); await this.remove_joins(datum); await this.config.remove(datum, options.write_bulks || "remove"); } return datum; }; } export default Queries;