UNPKG

para-client-js

Version:

JavaScript Client for Para

1,681 lines (1,675 loc) 66.4 kB
'use strict'; Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) { __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } } } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let assert = require("assert"); assert = __toESM(assert, 1); let superagent = require("superagent"); superagent = __toESM(superagent, 1); let aws4 = require("aws4"); aws4 = __toESM(aws4, 1); //#region lib/ParaObject.js var ParaObject = class { constructor(id, type) { this.id = id || null; this.type = type || "sysprop"; this.name = "ParaObject"; this.stored = true; this.indexed = true; this.cached = true; this.version = 0; } /** * The id of an object. Usually an autogenerated unique string of numbers. * * @return the id */ getId() { return this.id; } /** * Sets a new id. Must not be null or empty. * * @param {String} id the new id */ setId(id) { this.id = id; } /** * The name of the object. Can be anything. * * @return {String} the name. default: [type id] */ getName() { return this.name; } /** * Sets a new name. Must not be null or empty. * * @param {String} name the new name */ setName(name) { this.name = name; } /** * The application name. Added to support multiple separate apps. * Every object must belong to an app. * * @return {String} the app id (name). default: para */ getAppid() { return this.appid; } /** * Sets a new app name. Must not be null or empty. * * @param {String} appid the new app id (name) */ setAppid(appid) { this.appid = appid; } /** * The id of the parent object. * * @return {String} the id of the parent or null */ getParentid() { return this.parentid; } /** * Sets a new parent id. Must not be null or empty. * * @param {String} parentid a new id */ setParentid(parentid) { this.parentid = parentid; } /** * The name of the object's class. This is equivalent to {@link Class#getSimpleName()}.toLowerCase(). * * @return {String} the simple name of the class */ getType() { return this.type; } /** * Sets a new object type. Must not be null or empty. * * @param {String} type a new type */ setType(type) { this.type = type; } /** * The id of the user who created this. Should point to a {@link User} id. * * @return {String} the id or null */ getCreatorid() { return this.creatorid; } /** * Sets a new creator id. Must not be null or empty. * * @param {String} creatorid a new id */ setCreatorid(creatorid) { this.creatorid = creatorid; } /** * The URI of this object. For example: /user/123. * * @return {String} the URI */ getObjectURI() { var def = "/" + urlEncode$1(this.getType()); return this.id ? def + "/" + urlEncode$1(this.id) : def; } /** * The time when the object was created, in milliseconds. * * @return {Number} the timestamp of creation */ getTimestamp() { return this.timestamp; } /** * Sets the timestamp. * * @param {Number} timestamp a new timestamp in milliseconds. */ setTimestamp(timestamp) { this.timestamp = timestamp; } /** * The last time this object was updated. Timestamp in ms. * * @return {Number} timestamp in milliseconds */ getUpdated() { return this.updated; } /** * Sets the last updated timestamp. * * @param {Number} updated a new timestamp */ setUpdated(updated) { this.updated = updated; } /** * The tags associated with this object. Tags must not be null or empty. * * @return {Array} a set of tags, or an empty set */ getTags() { return this.id; } /** * Merges the given tags with existing tags. * * @param {Array} tags the additional tags, or clears all tags if set to null */ setTags(tags) { this.tags = tags; } /** * The votes associated with this object. * * @return {Number} votes or 0 */ getVotes() { return this.votes; } /** * Sets the votes. * * @param {Number} votes */ setVotes(votes) { this.votes = votes; } /** * The version of this object. * * @return {Number} version */ getVersion() { return this.version; } /** * Sets the version. * * @param {Number} version */ setVersion(version) { this.version = version; } /** * Boolean flag which controls whether this object is stored * in the database or not. Default is true. * * @return {Boolean} true if this object is stored in DB. */ getStored() { return this.stored; } /** * Sets the "isStored" flag. * * @param {Boolean} isStored when set to true, object is stored in DB. */ setStored(isStored) { this.stored = isStored; } /** * Boolean flat which controls whether this object is indexed * by the search engine. Default is true. * * @return {Boolean} true if this object is indexed */ getIndexed() { return this.indexed; } /** * Sets the "isIndexed" flag. * * @param {Boolean} isIndexed when set to true, object is indexed. */ setIndexed(isIndexed) { this.indexed = isIndexed; } /** * Boolean flat which controls whether this object is cached. * Default is true. * * @return {Boolean} true if this object is cached on update() and create(). */ getCached() { return this.cached; } /** * Sets the "isCached" flag. * * @param {Boolean} isCached when set to true, object is cached. */ setCached(isCached) { this.cached = isCached; } /** * Populates this object with data from a map. * @param {Object} map * @return {ParaObject} this */ setFields(map) { if (map && map instanceof Object) for (var key in map) this[key] = map[key]; return this; } }; function urlEncode$1(path) { return encodeURIComponent(path).replace(/[!'()*]/g, function(c) { return "%" + c.charCodeAt(0).toString(16).toUpperCase(); }); } //#endregion //#region lib/Pager.js /** * This class stores pagination data. It limits the results for queries in the DAO * and Search objects and also counts the total number of results that are returned. * @author Alex Bogdanovski <alex@erudika.com> * @param {Number} page page number to start from * @param {String} sortby sort by field * @param {Boolean} desc sort in descending or ascending order * @param {Number} limit limits the results * * @property {Number} count the total number of results * @property {String} lastKey reserved use * @property {Array} select selected fields filter for returning only part of an object * @returns {Pager} a pager */ var Pager = class { constructor(page, sortby, desc, limit) { this.page = page || 1; this.count = 0; this.sortby = sortby || null; this.desc = desc || true; this.limit = limit || 30; this.name = ""; this.lastKey = null; this.select = null; } }; //#endregion //#region lib/Constraint.js /** * Represents a validation constraint. * @author Alex Bogdanovski <alex@erudika.com> * @param {String} constraintName name * @param {Object} constraintPayload payload * @returns {Constraint} */ var Constraint = class Constraint { constructor(constraintName, constraintPayload) { var name = constraintName; var payload = constraintPayload; /** * The constraint name. * @returns {String} a name */ this.getName = function() { return name; }; /** * Sets the name of the constraint. * @param {String} n name */ this.setName = function(n) { name = n; }; /** * The payload (a map) * @returns {Object} an object */ this.getPayload = function() { return payload; }; /** * Sets the payload. * @param {Object} p the payload object */ this.setPayload = function(p) { payload = p; }; } /** * The 'required' constraint - marks a field as required. * @returns {Constraint} */ static required() { return new Constraint("required", { message: "messages.required" }); } /** * The 'min' constraint - field must contain a number larger than or equal to min. * @param {Number} min the minimum value * @returns {Constraint} */ static min(min) { return new Constraint("min", { value: min || 0, message: "messages.min" }); } /** * The 'max' constraint - field must contain a number smaller than or equal to max. * @param {Number} max the maximum value * @returns {Constraint} */ static max(max) { return new Constraint("max", { value: max || 0, message: "messages.max" }); } /** * The 'size' constraint - field must be a String, Object or Array * with a given minimum and maximum length. * @param {Number} min the minimum length * @param {Number} max the maximum length * @returns {Constraint} */ static size(min, max) { return new Constraint("size", { min: min || 0, max: max || 0, message: "messages.size" }); } /** * The 'digits' constraint - field must be a Number or String containing digits where the * number of digits in the integral part is limited by 'integer', and the * number of digits for the fractional part is limited * by 'fraction'. * @param {Number} i the max number of digits for the integral part * @param {Number} f the max number of digits for the fractional part * @returns {Constraint} */ static digits(i, f) { return new Constraint("digits", { integer: i || 0, fraction: f || 0, message: "messages.digits" }); } /** * The 'pattern' constraint - field must contain a value matching a regular expression. * @param {String} regex a regular expression * @returns {Constraint} */ static pattern(regex) { return new Constraint("pattern", { value: regex || "", message: "messages.pattern" }); } /** * The 'email' constraint - field must contain a valid email. * @returns {Constraint} */ static email() { return new Constraint("email", { message: "messages.email" }); } /** * The 'falsy' constraint - field value must not be equal to 'true'. * @returns {Constraint} */ static falsy() { return new Constraint("false", { message: "messages.false" }); } /** * The 'truthy' constraint - field value must be equal to 'true'. * @returns {Constraint} */ static truthy() { return new Constraint("true", { message: "messages.true" }); } /** * The 'future' constraint - field value must be a Date or a timestamp in the future. * @returns {Constraint} */ static future() { return new Constraint("future", { message: "messages.future" }); } /** * The 'past' constraint - field value must be a Date or a timestamp in the past. * @returns {Constraint} */ static past() { return new Constraint("past", { message: "messages.past" }); } /** * The 'url' constraint - field value must be a valid URL. * @returns {Constraint} */ static url() { return new Constraint("url", { message: "messages.url" }); } }; //#endregion //#region lib/index.js var err = console.error; const DEFAULT_ENDPOINT = "https://paraio.com"; const DEFAULT_PATH = "/v1/"; const JWT_PATH = "/jwt_auth"; const SEPARATOR = ":"; const { sign } = aws4.default; /** * JavaScript client for communicating with a Para API server. * @param {String} accessKey Para access key * @param {String} secretKey Para access key * @param {Object} options * @property {String} endpoint the API endpoint (default: paraio.com) * @property {String} apiPath the request path (default: /v1/) * @author Alex Bogdanovski <alex@erudika.com> */ var ParaClient = class { constructor(accessKey, secretKey, options) { if (!secretKey || isEmpty(secretKey.trim())) console.warn("Secret key not provided. Make sure you call 'signIn()' first."); options = options || {}; this.accessKey = accessKey; this.endpoint = options.endpoint || DEFAULT_ENDPOINT; this.apiPath = options.apiPath || DEFAULT_PATH; this.apiRequestTimeout = options.apiRequestTimeout || 120 * 1e3; this.tokenKey = null; this.tokenKeyExpires = null; this.tokenKeyNextRefresh = null; if (!endsWith(this.apiPath, "/")) this.apiPath += "/"; var that = this; var secret = secretKey; this.getFullPath = function(resourcePath) { if (resourcePath && startsWith(resourcePath, JWT_PATH)) { if ((that.apiPath.match(/\//g) || []).length > 2) return that.apiPath.substring(0, that.apiPath.indexOf("/", 1)) + resourcePath; return resourcePath; } if (!resourcePath) resourcePath = ""; else if (resourcePath[0] === "/") resourcePath = resourcePath.substring(1); return that.apiPath + resourcePath; }; this.setSecret = function(sec) { secret = sec; }; /** * Clears the JWT token from memory, if such exists. */ this.clearAccessToken = function() { that.tokenKey = null; that.tokenKeyExpires = null; that.tokenKeyNextRefresh = null; }; /** * @returns the JWT access token, or null if not signed in */ this.getAccessToken = function() { return that.tokenKey; }; /** * Sets the JWT access token. * @param {String} token a valid token */ this.setAccessToken = function(token) { if (token && token.length > 1) try { var parts = token.split("."); var decoded = JSON.parse(decode(parts[1])); if (decoded && decoded["exp"]) { that.tokenKeyExpires = decoded["exp"]; that.tokenKeyNextRefresh = decoded["refresh"]; } } catch (e) { that.tokenKeyExpires = null; that.tokenKeyNextRefresh = null; } that.tokenKey = token; }; /** * @param {Function} fn callback (optional) * @returns {Promise} the version of Para server */ this.getServerVersion = async function(fn) { fn = fn || noop; return that.getEntity(that.invokeGet("")).then(function(result) { var ver = result.version || "unknown"; fn(ver); return ver; }); }; /** * Invoke a GET request to the Para API. * @param {String} resourcePath the subpath after '/v1/', should not start with '/' * @param {Object} params query parameters * @returns {Object} response */ this.invokeGet = async function(resourcePath, params) { return that.invokeSignedRequest("GET", that.endpoint, that.getFullPath(resourcePath), null, params); }; /** * Invoke a POST request to the Para API. * @param {String} resourcePath the subpath after '/v1/', should not start with '/' * @param {Object} entity request body * @returns {Object} response */ this.invokePost = async function(resourcePath, entity) { return that.invokeSignedRequest("POST", that.endpoint, that.getFullPath(resourcePath), null, null, entity); }; /** * Invoke a PUT request to the Para API. * @param {String} resourcePath the subpath after '/v1/', should not start with '/' * @param {Object} entity request body * @returns {Object} response */ this.invokePut = async function(resourcePath, entity) { return that.invokeSignedRequest("PUT", that.endpoint, that.getFullPath(resourcePath), null, null, entity); }; /** * Invoke a PATCH request to the Para API. * @param {String} resourcePath the subpath after '/v1/', should not start with '/' * @param {Object} entity request body * @returns {Object} response */ this.invokePatch = async function(resourcePath, entity) { return that.invokeSignedRequest("PATCH", that.endpoint, that.getFullPath(resourcePath), null, null, entity); }; /** * Invoke a DELETE request to the Para API. * @param {String} resourcePath the subpath after '/v1/', should not start with '/' * @param {Object} params query parameters * @returns {Object} response */ this.invokeDelete = async function(resourcePath, params) { return that.invokeSignedRequest("DELETE", that.endpoint, that.getFullPath(resourcePath), null, params); }; this.invokeSignedRequest = async function(httpMethod, endpointURL, reqPath, headers, params, jsonEntity) { if (!accessKey || isEmpty(accessKey.trim())) throw new Error("Blank access key: " + httpMethod + " " + reqPath); var doSign = true; if (!secret && !that.tokenKey && isEmpty(headers)) { headers = { Authorization: "Anonymous " + accessKey }; doSign = false; } var host = endpointURL; if (startsWith(endpointURL, "http://")) host = endpointURL.substring(7); else if (startsWith(endpointURL, "https://")) host = endpointURL.substring(8); var opts = { service: "para", method: httpMethod, host, path: uriEncodeAWSV4(reqPath), headers: headers || {} }; if (params && params instanceof Object && !isEmpty(params)) { opts.path += "?"; var paramsObj = {}; for (var key in params) { var value = params[key]; if (isArray(value)) { if (!isEmpty(value)) paramsObj[key] = value[0] !== null ? value[0] : ""; } else paramsObj[key] = value !== null ? value : ""; } opts.path += new URLSearchParams(paramsObj).toString(); } if (jsonEntity) { opts.body = JSON.stringify(jsonEntity); opts.headers["Content-Type"] = "application/json; charset=UTF-8"; } if (that.tokenKey !== null) { if (!(httpMethod === "GET" && reqPath === JWT_PATH)) await that.refreshToken(); opts.headers["Authorization"] = "Bearer " + that.tokenKey; } else if (doSign) { opts.doNotEncodePath = true; sign(opts, { accessKeyId: accessKey, secretAccessKey: secret }); } if (typeof window !== "undefined") delete opts.headers["Host"]; opts.headers["User-Agent"] = "Para client for JavaScript"; try { return (0, superagent.default)(opts.method, endpointURL + reqPath).query(params).set(opts.headers).timeout({ response: that.apiRequestTimeout, deadline: that.apiRequestTimeout }).send(opts.body); } catch (e) { err("ParaClient request failed: " + e); return Promise.reject(e); } }; /** * Parses a search query response and extracts the objects from it. * @param {String} queryType type of search query * @param {Object} params query params * @param {Function} fn callback * @returns {Object} response */ this.find = async function(queryType, params, fn) { if (params && params instanceof Object && !isEmpty(params)) { var qType = queryType ? "/" + queryType : "/default"; if (!params["type"]) return that.getEntity(that.invokeGet("search" + qType, params), fn); else return that.getEntity(that.invokeGet(params["type"] + "/search" + qType, params), fn); } else { var res = { items: [], totalHits: 0 }; fn(res); return resolve(res); } }; /** * Deserializes a Response object to POJO of some type. * @param {Object} req request * @param {Function} callback callback * @param {Boolean} returnRawJSON true if raw JSON should be returned as string * @returns {Object} a ParaObject */ this.getEntity = async function(req, callback, returnRawJSON) { callback = callback || noop; var rawJSON = isUndefined(returnRawJSON) ? true : returnRawJSON; return req.then(function(res) { var code = res.status; if (code === 200 || code === 201 || code === 304) if (rawJSON) { var result; try { if (!isEmpty(res.body) || res.text === "{ }" || res.text === "{}") result = res.body; else result = res.text; } catch (exc) { result = res.text; } callback(result); return resolve(result); } else { var obj = new ParaObject(); obj.setFields(res.body); callback(obj); return resolve(obj); } else if (code !== 404 || code !== 304 || code !== 204) { var error = res.body || /* @__PURE__ */ new Error("ParaClient request failed."); if (error && error["code"]) err((error["message"] ? error["message"] : "error") + " - " + error["code"]); else err(code + " - " + res.text); callback(null, error); return Promise.reject(error); } else { var error1 = /* @__PURE__ */ new Error("ParaClient request failed."); callback(null, error1); reject(error1); } }); }; /** * Deserializes ParaObjects from a JSON array (the "items:[]" field in search results). * @param {Array} items a list of deserialized maps * @returns {Array} a list of ParaObjects */ this.getItemsFromList = function(items) { if (items && items instanceof Array && !isEmpty(items)) { var objects = []; for (var item of items) if (item) { var p = new ParaObject(); p.setFields(item); objects.push(p); } return objects; } return []; }; /** * Converts a list of Maps to a List of ParaObjects, at a given path within the JSON tree structure. * @param {Object} result the response body for an API request * @param {String} at the path (field) where the array of objects is located * @param {Pager} pager a pager * @returns {Array} a list of ParaObjects */ this.getItemsAt = function(result, at, pager) { if (result && at && result[at]) { if (pager && result.totalHits) pager.count = result.totalHits; if (pager && result.lastKey) pager.lastKey = result.lastKey; return that.getItemsFromList(result[at]); } return []; }; /** * Converts a list of Maps to a List of ParaObjects. * @param {Object} result the response body for an API request * @param {Pager} pager a pager * @returns {Array} a list of ParaObjects */ this.getItems = function(result, pager) { return that.getItemsAt(result, "items", pager); }; /** * Converts a {Pager} object to query parameters. * @param {Pager} pager a pager * @returns {Object} parameters map */ this.pagerToParams = function(pager) { var map = {}; if (pager) { map["page"] = pager.page; map["desc"] = pager.desc; map["limit"] = pager.limit; if (pager.lastKey) map["lastKey"] = pager.lastKey; if (pager.sortby) map["sort"] = pager.sortby; if (pager.select && pager.select.length) map["select"] = pager.select; } return map; }; } /** * Returns the App for the current access key (appid). * @param {Function} fn callback (optional) * @returns {Promise} a promise */ async getApp(fn) { return this.me(fn); } /** * Persists an object to the data store. If the object's type and id are given, * then the request will be a PUT request and any existing object will be * overwritten. * @param {ParaObject} obj the object to create * @param {Function} fn callback (optional) * @returns {Promise} the same object with assigned id or null if not created. */ async create(obj, fn) { fn = fn || noop; checkParaObject(obj); if (!obj) { fn(null); return resolve(null); } if (!obj.getId() || !obj.getType()) return this.getEntity(this.invokePost(urlEncode(obj.getType()), obj), fn, false); else return this.getEntity(this.invokePut(obj.getObjectURI(), obj), fn, false); } /** * Retrieves an object from the data store. * @param {String} type the type of the object * @param {String} id the id of the object * @param {Function} fn callback (optional) * @returns {Promise} the retrieved object or null if not found */ async read(type, id, fn) { fn = fn || noop; if (!id) { fn(null); return resolve(null); } if (!type) return this.getEntity(this.invokeGet("_id/" + urlEncode(id)), fn, false); else return this.getEntity(this.invokeGet(urlEncode(type) + "/" + urlEncode(id)), fn, false); } /** * Updates an object permanently. Supports partial updates. * @param {ParaObject} obj the object to update * @param {Function} fn callback (optional) * @returns {Promise} the updated object */ async update(obj, fn) { fn = fn || noop; checkParaObject(obj); if (!obj) { fn(null); return resolve(null); } return this.getEntity(this.invokePatch(obj.getObjectURI(), obj), fn, false); } /** * Deletes an object permanently. * @param {ParaObject} obj object to delete * @param {Function} fn callback (optional) * @returns {Promise} promise */ async delete(obj, fn) { fn = fn || noop; checkParaObject(obj); if (obj) return this.getEntity(this.invokeDelete(obj.getObjectURI()), fn); else { fn(null); return resolve(null); } } /** * Saves multiple objects to the data store. * @param {Array} objects a list of ParaObjects to create * @param {Function} fn callback (optional) * @returns {Promise} a list of objects */ async createAll(objects, fn) { fn = fn || noop; checkParaObjects(objects); if (!objects || !isArray(objects) || !objects[0]) { fn([]); return resolve([]); } var that = this; return this.getEntity(this.invokePost("_batch", objects)).then(function(result) { var res = that.getItemsFromList(result); fn(res); return res; }); } /** * Retrieves multiple objects from the data store. * @param {Array} keys a list of object ids * @param {Function} fn callback (optional) * @returns {Promise} a list of objects */ async readAll(keys, fn) { fn = fn || noop; if (!keys || !isArray(keys) || isEmpty(keys)) { fn([]); return resolve([]); } var that = this; return this.getEntity(this.invokeGet("_batch", { ids: keys })).then(function(result) { var res = that.getItemsFromList(result); fn(res); return res; }); } /** * Updates multiple objects. * @param {Array} objects a list of ParaObjects to update * @param {Function} fn callback (optional) * @returns {Promise} a list of objects */ async updateAll(objects, fn) { fn = fn || noop; checkParaObjects(objects); if (!objects || !isArray(objects) || isEmpty(objects)) { fn([]); return resolve([]); } var that = this; return this.getEntity(this.invokePatch("_batch", objects)).then(function(result) { var res = that.getItemsFromList(result); fn(res); return res; }); } /** * Deletes multiple objects. * @param {Function} fn callback (optional) * @param {Array} keys the ids of the objects to delete * @returns {Promise} promise */ async deleteAll(keys, fn) { fn = fn || noop; if (keys && isArray(keys)) return this.getEntity(this.invokeDelete("_batch", { ids: keys }), fn); else { fn(null); return resolve(null); } } /** * Returns a list all objects found for the given type. * The result is paginated so only one page of items is returned, at a time. * @param {String} type the type of objects to search for * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of objects */ async list(type, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); if (!type) { fn([]); return resolve([]); } var that = this; return this.getEntity(this.invokeGet(urlEncode(type), this.pagerToParams(pager))).then(function(result) { var res = that.getItems(result, pager); fn(res); return res; }); } /** * Simple id search. * @param {String} id the id * @param {Function} fn callback (optional) * @returns {Promise} the object if found or null */ async findById(id, fn) { fn = fn || noop; var that = this; return this.find("id", { id }).then(function(results) { var list = that.getItems(results); var res = isEmpty(list) ? null : list; fn(res); return res; }); } /** * Simple multi id search. * @param {Array} ids a list of ids to search for * @param {Function} fn callback (optional) * @returns {Promise} a list of objects if found or [] */ async findByIds(ids, fn) { fn = fn || noop; var that = this; return this.find("ids", { ids }).then(function(results) { var res = that.getItems(results); fn(res); return res; }); } /** * Search for address objects in a radius of X km from a given point. * @param {String} type the type of object to search for * @param {String} query the query string * @param {Number} radius the radius of the search circle * @param {Number} lat latitude * @param {Number} lng longitude * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findNearby(type, query, radius, lat, lng, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { latlng: lat + "," + lng, radius, q: query, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("nearby", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Searches for objects that have a property which value starts with a given prefix. * @param {String} type the type of object to search for * @param {String} field the property name of an object * @param {String} prefix the prefix * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findPrefix(type, field, prefix, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { field, prefix, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("prefix", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Simple query string search. This is the basic search method. * @param {String} type the type of object to search for * @param {String} query the query string * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findQuery(type, query, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { q: query, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Searches within a nested field. The objects of the given type must contain a nested field "nstd". * @param {String} type the type of object to search for * @param {String} field the name of the field to target (within a nested field "nstd") * @param {String} query the query string * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findNestedQuery(type, field, query, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { q: query, field, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("nested", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Searches for objects that have similar property values to a given text. A "find like this" query. * @param {String} type the type of object to search for * @param {String} filterKey exclude an object with this key from the results (optional) * @param {Array} fields a list of property names * @param {String} liketext text to compare to * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findSimilar(type, filterKey, fields, liketext, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { fields: fields || null, filterid: filterKey, like: liketext, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("similar", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Searches for objects tagged with one or more tags. * @param {String} type the type of object to search for * @param {Array} tags the list of tags * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findTagged(type, tags, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { tags: tags || null, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("tagged", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Searches for Tag objects. * This method might be deprecated in the future. * @param {String} keyword the tag keyword to search for * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findTags(keyword, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); keyword = keyword ? keyword + "*" : "*"; return this.findWildcard("tag", "tag", keyword, pager, fn); } /** * Searches for objects having a property value that is in list of possible values. * @param {String} type the type of object to search for * @param {String} field the property name of an object * @param {Object} terms a map of terms (property values) * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findTermInList(type, field, terms, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { field, terms, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("in", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Searches for objects that have properties matching some given values. A terms query. * @param {String} type the type of object to search for * @param {Object} terms a map of fields (property names) to terms (property values) * @param {Boolean} matchAll match all terms. If true - AND search, if false - OR search * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findTerms(type, terms, matchAll, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); terms = terms || {}; matchAll = matchAll || true; var params = { matchall: matchAll }; var list = []; for (var key in terms) if (terms[key]) list.push(key + SEPARATOR + terms[key]); if (!isEmpty(terms)) params["terms"] = list; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("terms", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Searches for objects that have a property with a value matching a wildcard query. * @param {String} type the type of object to search for * @param {String} field the property name of an object * @param {String} wildcard wildcard query string. For example "cat*". * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of object found */ async findWildcard(type, field, wildcard, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { field, q: wildcard, type }; params = merge(params, this.pagerToParams(pager)); var that = this; return this.find("wildcard", params).then(function(results) { var res = that.getItems(results, pager); fn(res); return res; }); } /** * Counts indexed objects matching a set of terms/values. * @param {String} type the type of object to search for * @param {Object} terms a map of fields (property names) to terms (property values) * @param {Function} fn callback (optional) * @returns {Promise} the number of results found */ async getCount(type, terms, fn) { fn = fn || noop; if (type === null && terms === null) { fn(0); return resolve(0); } terms = terms || {}; var params = {}; var pager = new Pager(); var that = this; params["type"] = type; if (isEmpty(terms)) return this.find("count", params).then(function(results) { that.getItems(results, pager); var res = pager.count; fn(res); return res; }); else { var list = []; for (var key in terms) if (terms[key]) list.push(key + SEPARATOR + terms[key]); if (!isEmpty(terms)) params["terms"] = list; params["count"] = "true"; return this.find("terms", params).then(function(results) { that.getItems(results, pager); var res = pager.count; fn(res); return res; }); } } /** * Count the total number of links between this object and another type of object. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {Function} fn callback (optional) * @returns {Promise} the number of links for the given object */ async countLinks(obj, type2, fn) { fn = fn || noop; checkParaObject(obj); if (!obj || !obj.getId() || !type2) { fn(0); return resolve(0); } var params = {}; params["count"] = "true"; var pager = new Pager(); var url = obj.getObjectURI() + "/links/" + urlEncode(type2); var that = this; return this.getEntity(this.invokeGet(url, params)).then(function(result) { that.getItems(result, pager); var res = pager.count; fn(res); return res; }); } /** * Returns all objects linked to the given one. Only applicable to many-to-many relationships. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of linked objects */ async getLinkedObjects(obj, type2, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); checkParaObject(obj); if (!obj || !obj.getId() || !type2) { fn([]); return resolve([]); } var url = obj.getObjectURI() + "/links/" + urlEncode(type2); var that = this; return this.getEntity(this.invokeGet(url, this.pagerToParams(pager))).then(function(result) { var res = that.getItems(result, pager); fn(res); return res; }); } /** * Searches through all linked objects in many-to-many relationships. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {String} field the name of the field to target (within a nested field "nstd") * @param {String} query a query string * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of linked objects */ async findLinkedObjects(obj, type2, field, query, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); checkParaObject(obj); if (!obj || !obj.getId() || !type2) { fn([]); return resolve([]); } var params = { field, q: query || "*" }; params = merge(params, this.pagerToParams(pager)); var url = obj.getObjectURI() + "/links/" + urlEncode(type2); var that = this; return this.getEntity(this.invokeGet(url, params)).then(function(result) { var res = that.getItems(result, pager); fn(res); return res; }); } /** * Checks if this object is linked to another. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {String} id2 the other id * @param {Function} fn callback (optional) * @returns {Promise} true if the two are linked */ async isLinked(obj, type2, id2, fn) { fn = fn || noop; checkParaObject(obj); if (!obj || !obj.getId() || !type2 || !id2) { fn(false); return resolve(false); } var url = obj.getObjectURI() + "/links/" + urlEncode(type2) + "/" + urlEncode(id2); return this.getEntity(this.invokeGet(url)).then(function(result) { var res = result === "true"; fn(res); return res; }); } /** * Checks if a given object is linked to this one. * @param {ParaObject} obj the object to execute this method on * @param {ParaObject} toObj the other object * @param {Function} fn callback (optional) * @returns {Promise} true if linked */ async isLinkedToObject(obj, toObj, fn) { fn = fn || noop; checkParaObject(obj); checkParaObject(toObj); if (!obj || !obj.getId() || !toObj || !toObj.getId()) { fn(false); return resolve(false); } return this.isLinked(obj, toObj.getType(), toObj.getId(), fn); } /** * Links an object to this one in a many-to-many relationship. * Only a link is created. Objects are left untouched. * The type of the second object is automatically determined on read. * @param {ParaObject} obj the object to execute this method on * @param {String} id2 the other id * @param {Function} fn callback (optional) * @returns {Promise} the id of the Linker object that is created */ async link(obj, id2, fn) { fn = fn || noop; checkParaObject(obj); if (!obj || !obj.getId() || !id2) { fn(null); return resolve(null); } var url = obj.getObjectURI() + "/links/" + urlEncode(id2); return this.getEntity(this.invokePost(url), fn); } /** * Unlinks an object from this one. * Only a link is deleted. Objects are left untouched. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {String} id2 the other id * @param {Function} fn callback (optional) * @returns {Promise} promise */ async unlink(obj, type2, id2, fn) { fn = fn || noop; checkParaObject(obj); if (!obj || !obj.getId() || !type2 || !id2) { fn(null); return resolve(null); } var url = obj.getObjectURI() + "/links/" + urlEncode(type2) + "/" + urlEncode(id2); return this.getEntity(this.invokeDelete(url), fn); } /** * Unlinks all objects that are linked to this one. * Deletes all Linker objects. * Only the links are deleted. Objects are left untouched. * @param {ParaObject} obj the object to execute this method on * @param {Function} fn callback (optional) * @returns {Promise} promise */ async unlinkAll(obj, fn) { fn = fn || noop; checkParaObject(obj); if (!obj || !obj.getId()) { fn(null); return resolve(null); } var url = obj.getObjectURI() + "/links"; return this.getEntity(this.invokeDelete(url), fn); } /** * Count the total number of child objects for this object. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {Function} fn callback (optional) * @returns {Promise} the number of links */ async countChildren(obj, type2, fn) { fn = fn || noop; checkParaObject(obj); if (!obj || !obj.getId() || !type2) { fn(0); return resolve(0); } var params = {}; params["count"] = "true"; params["childrenonly"] = "true"; var pager = new Pager(); var url = obj.getObjectURI() + "/links/" + urlEncode(type2); var that = this; return this.getEntity(this.invokeGet(url, params)).then(function(result) { that.getItems(result, pager); var res = pager.count; fn(res); return res; }); } /** * Returns all child objects linked to this object. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {String} field the field name to use as filter * @param {String} term the field value to use as filter * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of ParaObject in a one-to-many relationship with this object */ async getChildren(obj, type2, field, term, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); checkParaObject(obj); if (!obj || !obj.getId() || !type2) { fn([]); return resolve([]); } var params = {}; params["childrenonly"] = "true"; if (field) params["field"] = field; if (term) params["term"] = term; params = merge(params, this.pagerToParams(pager)); var url = obj.getObjectURI() + "/links/" + urlEncode(type2); var that = this; return this.getEntity(this.invokeGet(url, params)).then(function(result) { var res = that.getItems(result, pager); fn(res); return res; }); } /** * Search through all child objects. Only searches child objects directly * connected to this parent via the `parentid` field. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {String} query a query string * @param {Pager} pager a Pager object * @param {Function} fn callback (optional) * @returns {Promise} a list of ParaObject in a one-to-many relationship with this object */ async findChildren(obj, type2, query, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); checkParaObject(obj); if (!obj || !obj.getId() || !type2) { fn([]); return resolve([]); } var params = { childrenonly: "true", q: query || "*" }; params = merge(params, this.pagerToParams(pager)); var url = obj.getObjectURI() + "/links/" + urlEncode(type2); var that = this; return this.getEntity(this.invokeGet(url, params)).then(function(result) { var res = that.getItems(result, pager); fn(res); return res; }); } /** * Deletes all child objects permanently. * @param {ParaObject} obj the object to execute this method on * @param {String} type2 the other type of object * @param {Function} fn callback (optional) * @returns {Promise} promise */ async deleteChildren(obj, type2, fn) { fn = fn || noop; checkParaObject(obj); if (!obj || !obj.getId() || !type2) { fn(null); return resolve(null); } var params = {}; params["childrenonly"] = "true"; var url = obj.getObjectURI() + "/links/" + urlEncode(type2); return this.getEntity(this.invokeDelete(url, params), fn); } /** * Generates a new unique id. * @param {Function} fn callback (optional) * @returns {Promise} a new id */ async newId(fn) { fn = fn || noop; return this.getEntity(this.invokeGet("utils/newid")).then(function(result) { var res = result ? result : ""; fn(res); return res; }); } /** * Returns the current timestamp. * @param {Function} fn callback (optional) * @returns {Promise} timestamp in milliseconds */ async getTimestamp(fn) { fn = fn || noop; return this.getEntity(this.invokeGet("utils/timestamp")).then(function(result) { var res = result ? result : 0; fn(res); return res; }); } /** * Formats a date in a specific format. * @param {String} format the date format * @param {String} locale the locale instance * @param {Function} fn callback (optional) * @returns {Promise} a formatted date */ async formatDate(format, locale, fn) { var params = { format: format || "", locale: locale || "US" }; return this.getEntity(this.invokeGet("utils/formatdate", params), fn); } /** * Converts spaces to dashes. * @param {String} str a string with spaces * @param {String} replaceWith a string to replace spaces with * @param {Function} fn callback (optional) * @returns {Promise} a string with no whitespace */ async noSpaces(str, replaceWith, fn) { var params = { string: str || "", replacement: replaceWith || "" }; return this.getEntity(this.invokeGet("utils/nospaces", params), fn); } /** * Strips all symbols, punctuation, whitespace and control chars from a string. * @param {String} str a dirty string * @param {Function} fn callback (optional) * @returns {Promise} a clean string */ async stripAndTrim(str, fn) { var params = { string: str || "" }; return this.getEntity(this.invokeGet("utils/nosymbols", params), fn); } /** * Converts Markdown to HTML * @param {String} markdownString some Markdown * @param {Function} fn callback (optional) * @returns {Promise} HTML */ async markdownToHtml(markdownString, fn) { var params = { md: markdownString || "" }; return this.getEntity(this.invokeGet("utils/md2html", params), fn); } /** * Returns the number of minutes, hours, months elapsed for a time delta (milliseconds). * @param {Number} delta the time delta between two events, in milliseconds * @param {Function} fn callback (optional) * @returns {Promise} a string like "5m", "1h" */ async approximately(delta, fn) { var params = { delta: delta || 0 }; return this.getEntity(this.invokeGet("utils/timeago", params), fn); } /** * Generates a new set of access/secret keys. * Old keys are discarded and invalid after this. * @param {Function} fn callback (optional) * @returns {Promise} a map of new credentials */ async newKeys(fn) { fn = fn || noop; var that = this; return this.getEntity(this.invokePost("_newkeys")).then(function(result) { var res = result || {}; if (res.secretKey && !isEmpty(res.secretKey.trim())) that.setSecret(res.secretKey); fn(res); return res; }); } /** * Returns all registered types for this App. * @param {Function} fn callback (optional) * @returns {Promise} a map of plural-singular form of all the registered types. */ async types(fn) { return this.getEntity(this.invokeGet("_types"), fn); } /** * Returns the number of objects for each existing type in this App. * @param {Function} fn callback (optional) * @returns {Promise} a map of singular object type to object count. */ async typesCount(fn) { return this.getEntity(this.invokeGet("_types", { count: "true" }), fn); } /** * Returns a User or an App that is currently authenticated. * @param {String} accessToken a valid JWT access token (optional) * @param {Function} fn callback (optional) * @returns {Promise} a ParaObject */ async me(accessToken, fn) { fn = isFunction(accessToken) ? accessToken : fn || noop; if (accessToken && isString(accessToken)) { var headers = { Authorization: startsWith(accessToken, "Bearer") ? accessToken : "Bearer " + accessToken }; return this.getEntity(this.invokeSignedRequest("GET", this.endpoint, this.getFullPath("_me"), headers), fn, false); } else return this.getEntity(this.invokeGe