UNPKG

para-client-js

Version:

JavaScript Client for Para

1,488 lines (1,459 loc) 60.8 kB
/* * Copyright 2013-2022 Erudika. https://erudika.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For issues and patches go to: https://github.com/erudika */ /* global encodeURIComponent */ 'use strict'; var err = console.error; import lodash from 'lodash'; import assert from 'assert'; import apiClient from 'superagent'; import aws4 from 'aws4'; import { Promise } from 'rsvp'; import ParaObject from './ParaObject.js'; import Pager from './Pager.js'; import Constraint from './Constraint.js'; var DEFAULT_ENDPOINT = "https://paraio.com"; var DEFAULT_PATH = "/v1/"; var JWT_PATH = "/jwt_auth"; var SEPARATOR = ":"; const { isEmpty, endsWith, startsWith, noop, isArray, isUndefined, isString, isFunction, merge, isInteger, isBoolean } = lodash; 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] */ export default class ParaClient { 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 * 1000; 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 {RSVP.Promise} the version of Para server */ this.getServerVersion = 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 = 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 = 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 = 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 = 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 = function (resourcePath, params) { return that.invokeSignedRequest("DELETE", that.endpoint, that.getFullPath(resourcePath), null, params); }; this.invokeSignedRequest = 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: host, path: uriEncodeAWSV4(reqPath), headers: headers || {} }; // make sure that only the first parameter value is used for generating the signature // multi-valued parameters are reduced to single value // there's no spec for this case, so choose first param in array 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) { // make sure you don't create an infinite loop! if (!(httpMethod === "GET" && reqPath === JWT_PATH)) { that.refreshToken(); } opts.headers["Authorization"] = "Bearer " + that.tokenKey; } else if (doSign) { opts.doNotEncodePath = true; sign(opts, { accessKeyId: accessKey, secretAccessKey: secret }); } if (typeof window !== "undefined") { // don't set the 'Host' header, the browser does that. 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 null; } /** * 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 = 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 = function (req, callback, returnRawJSON) { callback = callback || noop; var rawJSON = isUndefined(returnRawJSON) ? true : returnRawJSON; return new Promise(function (resolve, reject) { if (req) { req.end(function (e, res) { //console.log("DEBUG ", req.method, req.url, res.status); if (e) { callback(null, e); reject(e); } else { 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); resolve(result); } else { var obj = new ParaObject(); obj.setFields(res.body); callback(obj); resolve(obj); } } else if (code !== 404 || code !== 304 || code !== 204) { var error = res.body || new Error("ParaClient request failed."); if (error && error["code"]) { var msg = error["message"] ? error["message"] : "error"; err(msg + " - " + error["code"]); } else { err(code + " - " + res.text); } callback(null, error); reject(error); } else { var error1 = new Error("ParaClient request failed."); callback(null, error1); reject(error1); } } }); } else { var error2 = new Error("Request object is undefined."); callback(null, error2); reject(error2); } }); }; /** * 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 {RSVP.Promise} a promise */ getApp(fn) { return this.me(fn); } ///////////////////////////////////////////// // PERSISTENCE ///////////////////////////////////////////// /** * 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 {RSVP.Promise} the same object with assigned id or null if not created. */ 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 {RSVP.Promise} the retrieved object or null if not found */ 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 {RSVP.Promise} the updated object */ 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 {RSVP.Promise} promise */ 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 {RSVP.Promise} a list of objects */ 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 {RSVP.Promise} a list of objects */ 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 {RSVP.Promise} a list of objects */ 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 {RSVP.Promise} promise */ 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 {RSVP.Promise} a list of objects */ 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; }); } ///////////////////////////////////////////// // SEARCH ///////////////////////////////////////////// /** * Simple id search. * @param {String} id the id * @param {Function} fn callback (optional) * @returns {RSVP.Promise} the object if found or null */ findById(id, fn) { fn = fn || noop; var that = this; return this.find("id", { "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 {RSVP.Promise} a list of objects if found or [] */ findByIds(ids, fn) { fn = fn || noop; var that = this; return this.find("ids", { "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 {RSVP.Promise} a list of object found */ findNearby(type, query, radius, lat, lng, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "latlng": lat + "," + lng, "radius": radius, "q": query, "type": 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 {RSVP.Promise} a list of object found */ findPrefix(type, field, prefix, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "field": field, "prefix": prefix, "type": 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 {RSVP.Promise} a list of object found */ findQuery(type, query, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "q": query, "type": 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 {RSVP.Promise} a list of object found */ findNestedQuery(type, field, query, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "q": query, "field": field, "type": 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 {RSVP.Promise} a list of object found */ findSimilar(type, filterKey, fields, liketext, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "fields": fields || null, "filterid": filterKey, "like": liketext, "type": 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 {RSVP.Promise} a list of object found */ findTagged(type, tags, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "tags": tags || null, "type": 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 {RSVP.Promise} a list of object found */ 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 {RSVP.Promise} a list of object found */ findTermInList(type, field, terms, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "field": field, "terms": terms, "type": 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 {RSVP.Promise} a list of object found */ 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 {RSVP.Promise} a list of object found */ findWildcard(type, field, wildcard, pager, fn) { fn = fn || noop; fn = checkPager(pager, fn); var params = { "field": field, "q": wildcard, "type": 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 {RSVP.Promise} the number of results found */ 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; }); } } ///////////////////////////////////////////// // LINKS ///////////////////////////////////////////// /** * 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 {RSVP.Promise} the number of links for the given object */ 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 {RSVP.Promise} a list of linked objects */ 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 {RSVP.Promise} a list of linked objects */ 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": 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 {RSVP.Promise} true if the two are linked */ 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 {RSVP.Promise} true if linked */ 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 {RSVP.Promise} the id of the Linker object that is created */ 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 {RSVP.Promise} promise */ 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 {RSVP.Promise} promise */ 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 {RSVP.Promise} the number of links */ 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 {RSVP.Promise} a list of ParaObject in a one-to-many relationship with this object */ 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 {RSVP.Promise} a list of ParaObject in a one-to-many relationship with this object */ 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 {RSVP.Promise} promise */ 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); } ///////////////////////////////////////////// // UTILS ///////////////////////////////////////////// /** * Generates a new unique id. * @param {Function} fn callback (optional) * @returns {RSVP.Promise} a new id */ 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 {RSVP.Promise} timestamp in milliseconds */ 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 {RSVP.Promise} a formatted date */ 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 {RSVP.Promise} a string with no whitespace */ 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 {RSVP.Promise} a clean string */ 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 {RSVP.Promise} HTML */ 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 {RSVP.Promise} a string like "5m", "1h" */ approximately(delta, fn) { var params = { "delta": delta || 0 }; return this.getEntity(this.invokeGet("utils/timeago", params), fn); } ///////////////////////////////////////////// // MISC ///////////////////////////////////////////// /** * Generates a new set of access/secret keys. * Old keys are discarded and invalid after this. * @param {Function} fn callback (optional) * @returns {RSVP.Promise} a map of new credentials */ 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 {RSVP.Promise} a map of plural-singular form of all the registered types. */ 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 {RSVP.Promise} a map of singular object type to object count. */ 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 {RSVP.Promise} a ParaObject */ me(accessToken, fn) { fn = isFunction(accessToken) ? accessToken : (fn || noop); if (accessToken && isString(accessToken)) { var auth = startsWith(accessToken, "Bearer") ? accessToken : "Bearer " + accessToken; var headers = { "Authorization": auth }; 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 {RSVP.Promise} true if vote was successful */ 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 {RSVP.Promise} true if vote was successful */ voteDown(obj, voterid, expiresAfter, lockedAfter, fn) { fn = isFunction(expiresAfter) ? expiresAfter : (fn || noop); fn = fn || noop; if (!obj || isEmpty(voterid)) { fn(false); return resolve(false); } var body = { "_votedown": 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; }); } /** * Rebuilds the entire search index. * @param {String} destinationIndex an existing index as destination * @param {Function} fn callback (optional) * @returns {RSVP.Promise} a response object with properties "tookMillis" and "reindexed" */ rebuildIndex(destinationIndex, fn) { fn = fn || noop; if (!destinationIndex) { return this.getEntity(this.invokePost("_reindex"), fn); } else { return this.getEntity(this.invokeSignedRequest("POST", this.endpoint, this.getFullPath("_reindex"), {}, { 'destinationIndex': destinationIndex }), fn); } } ///////////////////////////////////////////// // Validation Constraints ///////////////////////////////////////////// /** * Returns the validation constraints map. * @param {String} type a type * @param {Function} fn callback (optional) * @returns {RSVP.Promise} a map containing all validation constraints. */ validationConstraints(type, fn) { return this.getEntity(this.invokeGet("_constraints/" + (urlEncode(type || ""))), fn); } /** * Add a new constraint for a given field. * @param {String} type a type * @param {String} field a field name * @param {Constraint} cons the constraint * @param {Function} fn callback (optional) * @returns {RSVP.Promise} a map containing all validation constraints for this type. */ addValidationConstraint(type, field, cons, fn) { fn = fn || noop; checkConstraint(cons); if (!type || !field || !cons) { fn({}); return resolve({}); } return this.getEntity(this.invokePut("_constraints/" + urlEncode(type) + "/" + field + "/" + cons.getName(), cons.getPayload()), fn); } /** * Removes a validation constraint for a given field. * @param {String} type a type * @param {String} field a field name * @param {String} constraintName the name of the constraint to remove * @param {Function} fn callback (optional) * @returns {RSVP.Promise} a map containing all validation constraints for this type. */ removeValidationConstraint(type, field, constraintName, fn) { fn = fn || noop; if (!type || !field || !constraintName) { fn({}); return resolve({}); } return this.getEntity(this.invokeDelete("_constraints/" + urlEncode(type) + "/" + field + "/" + constraintName), fn); } ///////////////////////////////////////////// // Resource Permissions ///////////////////////////////////////////// /** * Returns only the permissions for a given subject (user) of the current app. * If subject is not given returns the permissions for all subjects and resources for current app. * @param {String} subjectid the subject id (user id) * @param {Function} fn callback (optional) * @returns {RSVP.Promise} a map of subject ids to resource names to a list of allowed methods */ resourcePermissions(subjectid, fn) { if (!subjectid) { return this.getEntity(this.invokeGet("_permissions"), fn); } else { return this.getEntity(this.invokeGet("_permissions/" + urlEncode(subjectid)), fn); } } /** * Grants a permission to a subject that allows them to call the specified HTTP methods on a given resource. * @param {String} subjectid subject id (user id) * @param {String} resourcePath resource path or object type * @param {Array