para-client-js
Version:
JavaScript Client for Para
1,684 lines (1,680 loc) • 65 kB
JavaScript
import assert from "assert";
import apiClient from "superagent";
import aws4 from "aws4";
//#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;
/**
* 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 apiClient(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.invokeGet("_me"), fn, false);
}
/**
* Upvote an object and register the vote in DB.
* @param {ParaObject} obj the object to receive +1 votes
* @param {String} voterid the userid of the voter
* @param {Function} fn callback (optional)
* @returns {Promise} true if vote was successful
*/
async voteUp(obj, voterid, expiresAfter, lockedAfter, fn) {
fn = isFunction(expiresAfter) ? expiresAfter : fn || noop;
if (!obj || isEmpty(voterid)) {
fn(false);
return resolve(false);
}
var body = { _voteup: voterid };
if (isInteger(expiresAfter) && isInteger(lockedAfter)) {
body["_vote_expires_after"] = expiresAfter;
body["_vote_locked_after"] = lockedAfter;
}
return this.getEntity(this.invokePatch(obj.getObjectURI(), body)).then(function(result) {
var res = result === "true";
fn(res);
return res;
});
}
/**
* Downvote an object and register the vote in DB.
* @param {ParaObject} obj the object to receive +1 votes
* @param {String} voterid the userid of the voter
* @param {Function} fn callback (optional)
* @returns {Promise} true if vote was successful
*/
async voteDown(obj, voterid, expiresAfter, lockedAfter, fn) {
fn = isFunction(expiresAfter) ? expiresAfter : fn || noop;
fn = fn || noop;
if (!obj || i