para-client-js
Version:
JavaScript Client for Para
1 lines • 108 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["urlEncode"],"sources":["../lib/ParaObject.js","../lib/Pager.js","../lib/Constraint.js","../lib/index.js"],"sourcesContent":["/*\n * Copyright 2013-2026 Erudika. https://erudika.com\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * For issues and patches go to: https://github.com/erudika\n */\n/* global encodeURIComponent */\n\n'use strict';\n\nexport default class ParaObject {\n constructor(id, type) {\n this.id = id || null;\n this.type = type || 'sysprop';\n this.name = 'ParaObject';\n this.stored = true;\n this.indexed = true;\n this.cached = true;\n this.version = 0;\n }\n /**\n * The id of an object. Usually an autogenerated unique string of numbers.\n *\n * @return the id\n */\n getId() {\n return this.id;\n }\n /**\n * Sets a new id. Must not be null or empty.\n *\n * @param {String} id the new id\n */\n setId(id) {\n this.id = id;\n }\n /**\n * The name of the object. Can be anything.\n *\n * @return {String} the name. default: [type id]\n */\n getName() {\n return this.name;\n }\n /**\n * Sets a new name. Must not be null or empty.\n *\n * @param {String} name the new name\n */\n setName(name) {\n this.name = name;\n }\n /**\n * The application name. Added to support multiple separate apps.\n * Every object must belong to an app.\n *\n * @return {String} the app id (name). default: para\n */\n getAppid() {\n return this.appid;\n }\n /**\n * Sets a new app name. Must not be null or empty.\n *\n * @param {String} appid the new app id (name)\n */\n setAppid(appid) {\n this.appid = appid;\n }\n /**\n * The id of the parent object.\n *\n * @return {String} the id of the parent or null\n */\n getParentid() {\n return this.parentid;\n }\n /**\n * Sets a new parent id. Must not be null or empty.\n *\n * @param {String} parentid a new id\n */\n setParentid(parentid) {\n this.parentid = parentid;\n }\n /**\n * The name of the object's class. This is equivalent to {@link Class#getSimpleName()}.toLowerCase().\n *\n * @return {String} the simple name of the class\n */\n getType() {\n return this.type;\n }\n /**\n * Sets a new object type. Must not be null or empty.\n *\n * @param {String} type a new type\n */\n setType(type) {\n this.type = type;\n }\n /**\n * The id of the user who created this. Should point to a {@link User} id.\n *\n * @return {String} the id or null\n */\n getCreatorid() {\n return this.creatorid;\n }\n /**\n * Sets a new creator id. Must not be null or empty.\n *\n * @param {String} creatorid a new id\n */\n setCreatorid(creatorid) {\n this.creatorid = creatorid;\n }\n /**\n * The URI of this object. For example: /user/123.\n *\n * @return {String} the URI\n */\n getObjectURI() {\n var def = '/' + urlEncode(this.getType());\n return this.id ? def + '/' + urlEncode(this.id) : def;\n }\n /**\n * The time when the object was created, in milliseconds.\n *\n * @return {Number} the timestamp of creation\n */\n getTimestamp() {\n return this.timestamp;\n }\n /**\n * Sets the timestamp.\n *\n * @param {Number} timestamp a new timestamp in milliseconds.\n */\n setTimestamp(timestamp) {\n this.timestamp = timestamp;\n }\n /**\n * The last time this object was updated. Timestamp in ms.\n *\n * @return {Number} timestamp in milliseconds\n */\n getUpdated() {\n return this.updated;\n }\n /**\n * Sets the last updated timestamp.\n *\n * @param {Number} updated a new timestamp\n */\n setUpdated(updated) {\n this.updated = updated;\n }\n /**\n * The tags associated with this object. Tags must not be null or empty.\n *\n * @return {Array} a set of tags, or an empty set\n */\n getTags() {\n return this.id;\n }\n /**\n * Merges the given tags with existing tags.\n *\n * @param {Array} tags the additional tags, or clears all tags if set to null\n */\n setTags(tags) {\n this.tags = tags;\n }\n /**\n * The votes associated with this object.\n *\n * @return {Number} votes or 0\n */\n getVotes() {\n return this.votes;\n }\n /**\n * Sets the votes.\n *\n * @param {Number} votes\n */\n setVotes(votes) {\n this.votes = votes;\n }\n /**\n * The version of this object.\n *\n * @return {Number} version\n */\n getVersion() {\n return this.version;\n }\n /**\n * Sets the version.\n *\n * @param {Number} version\n */\n setVersion(version) {\n this.version = version;\n }\n /**\n * Boolean flag which controls whether this object is stored\n * in the database or not. Default is true.\n *\n * @return {Boolean} true if this object is stored in DB.\n */\n getStored() {\n return this.stored;\n }\n /**\n * Sets the \"isStored\" flag.\n *\n * @param {Boolean} isStored when set to true, object is stored in DB.\n */\n setStored(isStored) {\n this.stored = isStored;\n }\n /**\n * Boolean flat which controls whether this object is indexed\n * by the search engine. Default is true.\n *\n * @return {Boolean} true if this object is indexed\n */\n getIndexed() {\n return this.indexed;\n }\n /**\n * Sets the \"isIndexed\" flag.\n *\n * @param {Boolean} isIndexed when set to true, object is indexed.\n */\n setIndexed(isIndexed) {\n this.indexed = isIndexed;\n }\n /**\n * Boolean flat which controls whether this object is cached.\n * Default is true.\n *\n * @return {Boolean} true if this object is cached on update() and create().\n */\n getCached() {\n return this.cached;\n }\n /**\n * Sets the \"isCached\" flag.\n *\n * @param {Boolean} isCached when set to true, object is cached.\n */\n setCached(isCached) {\n this.cached = isCached;\n }\n /**\n * Populates this object with data from a map.\n * @param {Object} map\n * @return {ParaObject} this\n */\n setFields(map) {\n if (map && map instanceof Object) {\n for (var key in map) {\n this[key] = map[key];\n }\n }\n return this;\n }\n}\n\nfunction urlEncode(path) {\n return encodeURIComponent(path).replace(/[!'()*]/g, function (c) {\n return '%' + c.charCodeAt(0).toString(16).toUpperCase();\n });\n}\n","/*\n * Copyright 2013-2026 Erudika. https://erudika.com\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * For issues and patches go to: https://github.com/erudika\n */\n'use strict';\n\n/**\n * This class stores pagination data. It limits the results for queries in the DAO\n * and Search objects and also counts the total number of results that are returned.\n * @author Alex Bogdanovski <alex@erudika.com>\n * @param {Number} page page number to start from\n * @param {String} sortby sort by field\n * @param {Boolean} desc sort in descending or ascending order\n * @param {Number} limit limits the results\n *\n * @property {Number} count the total number of results\n * @property {String} lastKey reserved use\n * @property {Array} select selected fields filter for returning only part of an object\n * @returns {Pager} a pager\n */\nexport default class Pager {\n constructor(page, sortby, desc, limit) {\n this.page = page || 1;\n this.count = 0;\n this.sortby = sortby || null;\n this.desc = desc || true;\n this.limit = limit || 30;\n this.name = '';\n this.lastKey = null;\n this.select = null; // [field1,field2]\n }\n}\n","/*\n * Copyright 2013-2026 Erudika. https://erudika.com\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * For issues and patches go to: https://github.com/erudika\n */\n'use strict';\n\n/**\n * Represents a validation constraint.\n * @author Alex Bogdanovski <alex@erudika.com>\n * @param {String} constraintName name\n * @param {Object} constraintPayload payload\n * @returns {Constraint}\n */\nexport default class Constraint {\n constructor(constraintName, constraintPayload) {\n var name = constraintName;\n var payload = constraintPayload;\n\n /**\n * The constraint name.\n * @returns {String} a name\n */\n this.getName = function () {\n return name;\n };\n\n /**\n * Sets the name of the constraint.\n * @param {String} n name\n */\n this.setName = function (n) {\n name = n;\n };\n\n /**\n * The payload (a map)\n * @returns {Object} an object\n */\n this.getPayload = function () {\n return payload;\n };\n\n /**\n * Sets the payload.\n * @param {Object} p the payload object\n */\n this.setPayload = function (p) {\n payload = p;\n };\n }\n /**\n * The 'required' constraint - marks a field as required.\n * @returns {Constraint}\n */\n static required() {\n return new Constraint('required', { message: 'messages.required' });\n }\n /**\n * The 'min' constraint - field must contain a number larger than or equal to min.\n * @param {Number} min the minimum value\n * @returns {Constraint}\n */\n static min(min) {\n return new Constraint('min', {\n value: min || 0,\n message: 'messages.min'\n });\n }\n /**\n * The 'max' constraint - field must contain a number smaller than or equal to max.\n * @param {Number} max the maximum value\n * @returns {Constraint}\n */\n static max(max) {\n return new Constraint('max', {\n value: max || 0,\n message: 'messages.max'\n });\n }\n /**\n * The 'size' constraint - field must be a String, Object or Array\n * with a given minimum and maximum length.\n * @param {Number} min the minimum length\n * @param {Number} max the maximum length\n * @returns {Constraint}\n */\n static size(min, max) {\n return new Constraint('size', {\n min: min || 0,\n max: max || 0,\n message: 'messages.size'\n });\n }\n /**\n * The 'digits' constraint - field must be a Number or String containing digits where the\n * number of digits in the integral part is limited by 'integer', and the\n * number of digits for the fractional part is limited\n * by 'fraction'.\n * @param {Number} i the max number of digits for the integral part\n * @param {Number} f the max number of digits for the fractional part\n * @returns {Constraint}\n */\n static digits(i, f) {\n return new Constraint('digits', {\n integer: i || 0,\n fraction: f || 0,\n message: 'messages.digits'\n });\n }\n /**\n * The 'pattern' constraint - field must contain a value matching a regular expression.\n * @param {String} regex a regular expression\n * @returns {Constraint}\n */\n static pattern(regex) {\n return new Constraint('pattern', {\n value: regex || '',\n message: 'messages.pattern'\n });\n }\n /**\n * The 'email' constraint - field must contain a valid email.\n * @returns {Constraint}\n */\n static email() {\n return new Constraint('email', { message: 'messages.email' });\n }\n /**\n * The 'falsy' constraint - field value must not be equal to 'true'.\n * @returns {Constraint}\n */\n static falsy() {\n return new Constraint('false', { message: 'messages.false' });\n }\n /**\n * The 'truthy' constraint - field value must be equal to 'true'.\n * @returns {Constraint}\n */\n static truthy() {\n return new Constraint('true', { message: 'messages.true' });\n }\n /**\n * The 'future' constraint - field value must be a Date or a timestamp in the future.\n * @returns {Constraint}\n */\n static future() {\n return new Constraint('future', { message: 'messages.future' });\n }\n /**\n * The 'past' constraint - field value must be a Date or a timestamp in the past.\n * @returns {Constraint}\n */\n static past() {\n return new Constraint('past', { message: 'messages.past' });\n }\n /**\n * The 'url' constraint - field value must be a valid URL.\n * @returns {Constraint}\n */\n static url() {\n return new Constraint('url', { message: 'messages.url' });\n }\n}\n","/*\n * Copyright 2013-2026 Erudika. https://erudika.com\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * For issues and patches go to: https://github.com/erudika\n */\n/* global encodeURIComponent */\n\n'use strict';\n\nvar err = console.error;\nimport assert from 'assert';\nimport apiClient from 'superagent';\nimport aws4 from 'aws4';\nimport ParaObject from './ParaObject.js';\nimport Pager from './Pager.js';\nimport Constraint from './Constraint.js';\n\nconst DEFAULT_ENDPOINT = 'https://paraio.com';\nconst DEFAULT_PATH = '/v1/';\nconst JWT_PATH = '/jwt_auth';\nconst SEPARATOR = ':';\nconst { sign } = aws4;\n\n/**\n * JavaScript client for communicating with a Para API server.\n * @param {String} accessKey Para access key\n * @param {String} secretKey Para access key\n * @param {Object} options\n * @property {String} endpoint the API endpoint (default: paraio.com)\n * @property {String} apiPath the request path (default: /v1/)\n * @author Alex Bogdanovski <alex@erudika.com>\n */\nexport default class ParaClient {\n constructor(accessKey, secretKey, options) {\n if (!secretKey || isEmpty(secretKey.trim())) {\n console.warn(\"Secret key not provided. Make sure you call 'signIn()' first.\");\n }\n options = options || {};\n this.accessKey = accessKey;\n this.endpoint = options.endpoint || DEFAULT_ENDPOINT;\n this.apiPath = options.apiPath || DEFAULT_PATH;\n this.apiRequestTimeout = options.apiRequestTimeout || 120 * 1000;\n this.tokenKey = null;\n this.tokenKeyExpires = null;\n this.tokenKeyNextRefresh = null;\n if (!endsWith(this.apiPath, '/')) {\n this.apiPath += '/';\n }\n\n var that = this;\n var secret = secretKey;\n\n this.getFullPath = function (resourcePath) {\n if (resourcePath && startsWith(resourcePath, JWT_PATH)) {\n if ((that.apiPath.match(/\\//g) || []).length > 2) {\n return that.apiPath.substring(0, that.apiPath.indexOf('/', 1)) + resourcePath;\n }\n return resourcePath;\n }\n if (!resourcePath) {\n resourcePath = '';\n } else if (resourcePath[0] === '/') {\n resourcePath = resourcePath.substring(1);\n }\n return that.apiPath + resourcePath;\n };\n\n this.setSecret = function (sec) {\n secret = sec;\n };\n\n /**\n * Clears the JWT token from memory, if such exists.\n */\n this.clearAccessToken = function () {\n that.tokenKey = null;\n that.tokenKeyExpires = null;\n that.tokenKeyNextRefresh = null;\n };\n\n /**\n * @returns the JWT access token, or null if not signed in\n */\n this.getAccessToken = function () {\n return that.tokenKey;\n };\n\n /**\n * Sets the JWT access token.\n * @param {String} token a valid token\n */\n this.setAccessToken = function (token) {\n if (token && token.length > 1) {\n try {\n var parts = token.split('.');\n var decoded = JSON.parse(decode(parts[1]));\n if (decoded && decoded['exp']) {\n that.tokenKeyExpires = decoded['exp'];\n that.tokenKeyNextRefresh = decoded['refresh'];\n }\n } catch (e) {\n that.tokenKeyExpires = null;\n that.tokenKeyNextRefresh = null;\n }\n }\n that.tokenKey = token;\n };\n\n /**\n * @param {Function} fn callback (optional)\n * @returns {Promise} the version of Para server\n */\n this.getServerVersion = async function (fn) {\n fn = fn || noop;\n return that.getEntity(that.invokeGet('')).then(function (result) {\n var ver = result.version || 'unknown';\n fn(ver);\n return ver;\n });\n };\n\n /**\n * Invoke a GET request to the Para API.\n * @param {String} resourcePath the subpath after '/v1/', should not start with '/'\n * @param {Object} params query parameters\n * @returns {Object} response\n */\n this.invokeGet = async function (resourcePath, params) {\n return that.invokeSignedRequest(\n 'GET',\n that.endpoint,\n that.getFullPath(resourcePath),\n null,\n params\n );\n };\n\n /**\n * Invoke a POST request to the Para API.\n * @param {String} resourcePath the subpath after '/v1/', should not start with '/'\n * @param {Object} entity request body\n * @returns {Object} response\n */\n this.invokePost = async function (resourcePath, entity) {\n return that.invokeSignedRequest(\n 'POST',\n that.endpoint,\n that.getFullPath(resourcePath),\n null,\n null,\n entity\n );\n };\n\n /**\n * Invoke a PUT request to the Para API.\n * @param {String} resourcePath the subpath after '/v1/', should not start with '/'\n * @param {Object} entity request body\n * @returns {Object} response\n */\n this.invokePut = async function (resourcePath, entity) {\n return that.invokeSignedRequest(\n 'PUT',\n that.endpoint,\n that.getFullPath(resourcePath),\n null,\n null,\n entity\n );\n };\n\n /**\n * Invoke a PATCH request to the Para API.\n * @param {String} resourcePath the subpath after '/v1/', should not start with '/'\n * @param {Object} entity request body\n * @returns {Object} response\n */\n this.invokePatch = async function (resourcePath, entity) {\n return that.invokeSignedRequest(\n 'PATCH',\n that.endpoint,\n that.getFullPath(resourcePath),\n null,\n null,\n entity\n );\n };\n\n /**\n * Invoke a DELETE request to the Para API.\n * @param {String} resourcePath the subpath after '/v1/', should not start with '/'\n * @param {Object} params query parameters\n * @returns {Object} response\n */\n this.invokeDelete = async function (resourcePath, params) {\n return that.invokeSignedRequest(\n 'DELETE',\n that.endpoint,\n that.getFullPath(resourcePath),\n null,\n params\n );\n };\n\n this.invokeSignedRequest = async function (\n httpMethod,\n endpointURL,\n reqPath,\n headers,\n params,\n jsonEntity\n ) {\n if (!accessKey || isEmpty(accessKey.trim())) {\n throw new Error('Blank access key: ' + httpMethod + ' ' + reqPath);\n }\n var doSign = true;\n if (!secret && !that.tokenKey && isEmpty(headers)) {\n headers = { Authorization: 'Anonymous ' + accessKey };\n doSign = false;\n }\n var host = endpointURL;\n if (startsWith(endpointURL, 'http://')) {\n host = endpointURL.substring(7);\n } else if (startsWith(endpointURL, 'https://')) {\n host = endpointURL.substring(8);\n }\n\n var opts = {\n service: 'para',\n method: httpMethod,\n host: host,\n path: uriEncodeAWSV4(reqPath),\n headers: headers || {}\n };\n\n // make sure that only the first parameter value is used for generating the signature\n // multi-valued parameters are reduced to single value\n // there's no spec for this case, so choose first param in array\n if (params && params instanceof Object && !isEmpty(params)) {\n opts.path += '?';\n var paramsObj = {};\n for (var key in params) {\n var value = params[key];\n if (isArray(value)) {\n if (!isEmpty(value)) {\n paramsObj[key] = value[0] !== null ? value[0] : '';\n }\n } else {\n paramsObj[key] = value !== null ? value : '';\n }\n }\n opts.path += new URLSearchParams(paramsObj).toString();\n }\n\n if (jsonEntity) {\n opts.body = JSON.stringify(jsonEntity);\n opts.headers['Content-Type'] = 'application/json; charset=UTF-8';\n }\n\n if (that.tokenKey !== null) {\n // make sure you don't create an infinite loop!\n if (!(httpMethod === 'GET' && reqPath === JWT_PATH)) {\n await that.refreshToken();\n }\n opts.headers['Authorization'] = 'Bearer ' + that.tokenKey;\n } else if (doSign) {\n opts.doNotEncodePath = true;\n sign(opts, { accessKeyId: accessKey, secretAccessKey: secret });\n }\n\n if (typeof window !== 'undefined') {\n // don't set the 'Host' header, the browser does that.\n delete opts.headers['Host'];\n }\n opts.headers['User-Agent'] = 'Para client for JavaScript';\n try {\n return apiClient(opts.method, endpointURL + reqPath)\n .query(params)\n .set(opts.headers)\n .timeout({ response: that.apiRequestTimeout, deadline: that.apiRequestTimeout })\n .send(opts.body);\n } catch (e) {\n err('ParaClient request failed: ' + e);\n return Promise.reject(e);\n }\n };\n\n /**\n * Parses a search query response and extracts the objects from it.\n * @param {String} queryType type of search query\n * @param {Object} params query params\n * @param {Function} fn callback\n * @returns {Object} response\n */\n this.find = async function (queryType, params, fn) {\n if (params && params instanceof Object && !isEmpty(params)) {\n var qType = queryType ? '/' + queryType : '/default';\n if (!params['type']) {\n return that.getEntity(that.invokeGet('search' + qType, params), fn);\n } else {\n return that.getEntity(that.invokeGet(params['type'] + '/search' + qType, params), fn);\n }\n } else {\n var res = {\n items: [],\n totalHits: 0\n };\n fn(res);\n return resolve(res);\n }\n };\n\n /**\n * Deserializes a Response object to POJO of some type.\n * @param {Object} req request\n * @param {Function} callback callback\n * @param {Boolean} returnRawJSON true if raw JSON should be returned as string\n * @returns {Object} a ParaObject\n */\n this.getEntity = async function (req, callback, returnRawJSON) {\n callback = callback || noop;\n var rawJSON = isUndefined(returnRawJSON) ? true : returnRawJSON;\n return req.then(function (res) {\n //console.log(\"DEBUG \", req.method, req.url, res.status);\n var code = res.status;\n if (code === 200 || code === 201 || code === 304) {\n if (rawJSON) {\n var result;\n try {\n if (!isEmpty(res.body) || res.text === '{ }' || res.text === '{}') {\n result = res.body;\n } else {\n result = res.text;\n }\n } catch (exc) {\n result = res.text;\n }\n callback(result);\n return resolve(result);\n } else {\n var obj = new ParaObject();\n obj.setFields(res.body);\n callback(obj);\n return resolve(obj);\n }\n } else if (code !== 404 || code !== 304 || code !== 204) {\n var error = res.body || new Error('ParaClient request failed.');\n if (error && error['code']) {\n var msg = error['message'] ? error['message'] : 'error';\n err(msg + ' - ' + error['code']);\n } else {\n err(code + ' - ' + res.text);\n }\n callback(null, error);\n return Promise.reject(error);\n } else {\n var error1 = new Error('ParaClient request failed.');\n callback(null, error1);\n reject(error1);\n }\n });\n };\n\n /**\n * Deserializes ParaObjects from a JSON array (the \"items:[]\" field in search results).\n * @param {Array} items a list of deserialized maps\n * @returns {Array} a list of ParaObjects\n */\n this.getItemsFromList = function (items) {\n if (items && items instanceof Array && !isEmpty(items)) {\n var objects = [];\n for (var item of items) {\n if (item) {\n var p = new ParaObject();\n p.setFields(item);\n objects.push(p);\n }\n }\n return objects;\n }\n return [];\n };\n\n /**\n * Converts a list of Maps to a List of ParaObjects, at a given path within the JSON tree structure.\n * @param {Object} result the response body for an API request\n * @param {String} at the path (field) where the array of objects is located\n * @param {Pager} pager a pager\n * @returns {Array} a list of ParaObjects\n */\n this.getItemsAt = function (result, at, pager) {\n if (result && at && result[at]) {\n if (pager && result.totalHits) {\n pager.count = result.totalHits;\n }\n if (pager && result.lastKey) {\n pager.lastKey = result.lastKey;\n }\n return that.getItemsFromList(result[at]);\n }\n return [];\n };\n\n /**\n * Converts a list of Maps to a List of ParaObjects.\n * @param {Object} result the response body for an API request\n * @param {Pager} pager a pager\n * @returns {Array} a list of ParaObjects\n */\n this.getItems = function (result, pager) {\n return that.getItemsAt(result, 'items', pager);\n };\n\n /**\n * Converts a {Pager} object to query parameters.\n * @param {Pager} pager a pager\n * @returns {Object} parameters map\n */\n this.pagerToParams = function (pager) {\n var map = {};\n if (pager) {\n map['page'] = pager.page;\n map['desc'] = pager.desc;\n map['limit'] = pager.limit;\n if (pager.lastKey) {\n map['lastKey'] = pager.lastKey;\n }\n if (pager.sortby) {\n map['sort'] = pager.sortby;\n }\n if (pager.select && pager.select.length) {\n map['select'] = pager.select;\n }\n }\n return map;\n };\n }\n /**\n * Returns the App for the current access key (appid).\n * @param {Function} fn callback (optional)\n * @returns {Promise} a promise\n */\n async getApp(fn) {\n return this.me(fn);\n }\n /////////////////////////////////////////////\n //\t\t\t\t PERSISTENCE\n /////////////////////////////////////////////\n /**\n * Persists an object to the data store. If the object's type and id are given,\n * then the request will be a PUT request and any existing object will be\n * overwritten.\n * @param {ParaObject} obj the object to create\n * @param {Function} fn callback (optional)\n * @returns {Promise} the same object with assigned id or null if not created.\n */\n async create(obj, fn) {\n fn = fn || noop;\n checkParaObject(obj);\n if (!obj) {\n fn(null);\n return resolve(null);\n }\n if (!obj.getId() || !obj.getType()) {\n return this.getEntity(this.invokePost(urlEncode(obj.getType()), obj), fn, false);\n } else {\n return this.getEntity(this.invokePut(obj.getObjectURI(), obj), fn, false);\n }\n }\n /**\n * Retrieves an object from the data store.\n * @param {String} type the type of the object\n * @param {String} id the id of the object\n * @param {Function} fn callback (optional)\n * @returns {Promise} the retrieved object or null if not found\n */\n async read(type, id, fn) {\n fn = fn || noop;\n if (!id) {\n fn(null);\n return resolve(null);\n }\n if (!type) {\n return this.getEntity(this.invokeGet('_id/' + urlEncode(id)), fn, false);\n } else {\n return this.getEntity(this.invokeGet(urlEncode(type) + '/' + urlEncode(id)), fn, false);\n }\n }\n /**\n * Updates an object permanently. Supports partial updates.\n * @param {ParaObject} obj the object to update\n * @param {Function} fn callback (optional)\n * @returns {Promise} the updated object\n */\n async update(obj, fn) {\n fn = fn || noop;\n checkParaObject(obj);\n if (!obj) {\n fn(null);\n return resolve(null);\n }\n return this.getEntity(this.invokePatch(obj.getObjectURI(), obj), fn, false);\n }\n /**\n * Deletes an object permanently.\n * @param {ParaObject} obj object to delete\n * @param {Function} fn callback (optional)\n * @returns {Promise} promise\n */\n async delete(obj, fn) {\n fn = fn || noop;\n checkParaObject(obj);\n if (obj) {\n return this.getEntity(this.invokeDelete(obj.getObjectURI()), fn);\n } else {\n fn(null);\n return resolve(null);\n }\n }\n /**\n * Saves multiple objects to the data store.\n * @param {Array} objects a list of ParaObjects to create\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of objects\n */\n async createAll(objects, fn) {\n fn = fn || noop;\n checkParaObjects(objects);\n if (!objects || !isArray(objects) || !objects[0]) {\n fn([]);\n return resolve([]);\n }\n var that = this;\n return this.getEntity(this.invokePost('_batch', objects)).then(function (result) {\n var res = that.getItemsFromList(result);\n fn(res);\n return res;\n });\n }\n /**\n * Retrieves multiple objects from the data store.\n * @param {Array} keys a list of object ids\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of objects\n */\n async readAll(keys, fn) {\n fn = fn || noop;\n if (!keys || !isArray(keys) || isEmpty(keys)) {\n fn([]);\n return resolve([]);\n }\n var that = this;\n return this.getEntity(this.invokeGet('_batch', { ids: keys })).then(function (result) {\n var res = that.getItemsFromList(result);\n fn(res);\n return res;\n });\n }\n /**\n * Updates multiple objects.\n * @param {Array} objects a list of ParaObjects to update\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of objects\n */\n async updateAll(objects, fn) {\n fn = fn || noop;\n checkParaObjects(objects);\n if (!objects || !isArray(objects) || isEmpty(objects)) {\n fn([]);\n return resolve([]);\n }\n var that = this;\n return this.getEntity(this.invokePatch('_batch', objects)).then(function (result) {\n var res = that.getItemsFromList(result);\n fn(res);\n return res;\n });\n }\n /**\n * Deletes multiple objects.\n * @param {Function} fn callback (optional)\n * @param {Array} keys the ids of the objects to delete\n * @returns {Promise} promise\n */\n async deleteAll(keys, fn) {\n fn = fn || noop;\n if (keys && isArray(keys)) {\n return this.getEntity(this.invokeDelete('_batch', { ids: keys }), fn);\n } else {\n fn(null);\n return resolve(null);\n }\n }\n /**\n * Returns a list all objects found for the given type.\n * The result is paginated so only one page of items is returned, at a time.\n * @param {String} type the type of objects to search for\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of objects\n */\n async list(type, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n if (!type) {\n fn([]);\n return resolve([]);\n }\n var that = this;\n return this.getEntity(this.invokeGet(urlEncode(type), this.pagerToParams(pager))).then(\n function (result) {\n var res = that.getItems(result, pager);\n fn(res);\n return res;\n }\n );\n }\n /////////////////////////////////////////////\n //\t\t\t\t SEARCH\n /////////////////////////////////////////////\n /**\n * Simple id search.\n * @param {String} id the id\n * @param {Function} fn callback (optional)\n * @returns {Promise} the object if found or null\n */\n async findById(id, fn) {\n fn = fn || noop;\n var that = this;\n return this.find('id', { id: id }).then(function (results) {\n var list = that.getItems(results);\n var res = isEmpty(list) ? null : list;\n fn(res);\n return res;\n });\n }\n /**\n * Simple multi id search.\n * @param {Array} ids a list of ids to search for\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of objects if found or []\n */\n async findByIds(ids, fn) {\n fn = fn || noop;\n var that = this;\n return this.find('ids', { ids: ids }).then(function (results) {\n var res = that.getItems(results);\n fn(res);\n return res;\n });\n }\n /**\n * Search for address objects in a radius of X km from a given point.\n * @param {String} type the type of object to search for\n * @param {String} query the query string\n * @param {Number} radius the radius of the search circle\n * @param {Number} lat latitude\n * @param {Number} lng longitude\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findNearby(type, query, radius, lat, lng, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n latlng: lat + ',' + lng,\n radius: radius,\n q: query,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('nearby', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches for objects that have a property which value starts with a given prefix.\n * @param {String} type the type of object to search for\n * @param {String} field the property name of an object\n * @param {String} prefix the prefix\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findPrefix(type, field, prefix, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n field: field,\n prefix: prefix,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('prefix', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Simple query string search. This is the basic search method.\n * @param {String} type the type of object to search for\n * @param {String} query the query string\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findQuery(type, query, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n q: query,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches within a nested field. The objects of the given type must contain a nested field \"nstd\".\n * @param {String} type the type of object to search for\n * @param {String} field the name of the field to target (within a nested field \"nstd\")\n * @param {String} query the query string\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findNestedQuery(type, field, query, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n q: query,\n field: field,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('nested', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches for objects that have similar property values to a given text. A \"find like this\" query.\n * @param {String} type the type of object to search for\n * @param {String} filterKey exclude an object with this key from the results (optional)\n * @param {Array} fields a list of property names\n * @param {String} liketext text to compare to\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findSimilar(type, filterKey, fields, liketext, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n fields: fields || null,\n filterid: filterKey,\n like: liketext,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('similar', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches for objects tagged with one or more tags.\n * @param {String} type the type of object to search for\n * @param {Array} tags the list of tags\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findTagged(type, tags, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n tags: tags || null,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('tagged', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches for Tag objects.\n * This method might be deprecated in the future.\n * @param {String} keyword the tag keyword to search for\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findTags(keyword, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n keyword = keyword ? keyword + '*' : '*';\n return this.findWildcard('tag', 'tag', keyword, pager, fn);\n }\n /**\n * Searches for objects having a property value that is in list of possible values.\n * @param {String} type the type of object to search for\n * @param {String} field the property name of an object\n * @param {Object} terms a map of terms (property values)\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findTermInList(type, field, terms, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n field: field,\n terms: terms,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('in', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches for objects that have properties matching some given values. A terms query.\n * @param {String} type the type of object to search for\n * @param {Object} terms a map of fields (property names) to terms (property values)\n * @param {Boolean} matchAll match all terms. If true - AND search, if false - OR search\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findTerms(type, terms, matchAll, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n terms = terms || {};\n matchAll = matchAll || true;\n var params = {\n matchall: matchAll\n };\n var list = [];\n for (var key in terms) {\n if (terms[key]) {\n list.push(key + SEPARATOR + terms[key]);\n }\n }\n if (!isEmpty(terms)) {\n params['terms'] = list;\n }\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('terms', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches for objects that have a property with a value matching a wildcard query.\n * @param {String} type the type of object to search for\n * @param {String} field the property name of an object\n * @param {String} wildcard wildcard query string. For example \"cat*\".\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of object found\n */\n async findWildcard(type, field, wildcard, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n var params = {\n field: field,\n q: wildcard,\n type: type\n };\n params = merge(params, this.pagerToParams(pager));\n var that = this;\n return this.find('wildcard', params).then(function (results) {\n var res = that.getItems(results, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Counts indexed objects matching a set of terms/values.\n * @param {String} type the type of object to search for\n * @param {Object} terms a map of fields (property names) to terms (property values)\n * @param {Function} fn callback (optional)\n * @returns {Promise} the number of results found\n */\n async getCount(type, terms, fn) {\n fn = fn || noop;\n if (type === null && terms === null) {\n fn(0);\n return resolve(0);\n }\n terms = terms || {};\n var params = {};\n var pager = new Pager();\n var that = this;\n params['type'] = type;\n if (isEmpty(terms)) {\n return this.find('count', params).then(function (results) {\n that.getItems(results, pager);\n var res = pager.count;\n fn(res);\n return res;\n });\n } else {\n var list = [];\n for (var key in terms) {\n if (terms[key]) {\n list.push(key + SEPARATOR + terms[key]);\n }\n }\n if (!isEmpty(terms)) {\n params['terms'] = list;\n }\n params['count'] = 'true';\n return this.find('terms', params).then(function (results) {\n that.getItems(results, pager);\n var res = pager.count;\n fn(res);\n return res;\n });\n }\n }\n /////////////////////////////////////////////\n //\t\t\t\t LINKS\n /////////////////////////////////////////////\n /**\n * Count the total number of links between this object and another type of object.\n * @param {ParaObject} obj the object to execute this method on\n * @param {String} type2 the other type of object\n * @param {Function} fn callback (optional)\n * @returns {Promise} the number of links for the given object\n */\n async countLinks(obj, type2, fn) {\n fn = fn || noop;\n checkParaObject(obj);\n if (!obj || !obj.getId() || !type2) {\n fn(0);\n return resolve(0);\n }\n var params = {};\n params['count'] = 'true';\n var pager = new Pager();\n var url = obj.getObjectURI() + '/links/' + urlEncode(type2);\n var that = this;\n return this.getEntity(this.invokeGet(url, params)).then(function (result) {\n that.getItems(result, pager);\n var res = pager.count;\n fn(res);\n return res;\n });\n }\n /**\n * Returns all objects linked to the given one. Only applicable to many-to-many relationships.\n * @param {ParaObject} obj the object to execute this method on\n * @param {String} type2 the other type of object\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of linked objects\n */\n async getLinkedObjects(obj, type2, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n checkParaObject(obj);\n if (!obj || !obj.getId() || !type2) {\n fn([]);\n return resolve([]);\n }\n var url = obj.getObjectURI() + '/links/' + urlEncode(type2);\n var that = this;\n return this.getEntity(this.invokeGet(url, this.pagerToParams(pager))).then(function (result) {\n var res = that.getItems(result, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Searches through all linked objects in many-to-many relationships.\n * @param {ParaObject} obj the object to execute this method on\n * @param {String} type2 the other type of object\n * @param {String} field the name of the field to target (within a nested field \"nstd\")\n * @param {String} query a query string\n * @param {Pager} pager a Pager object\n * @param {Function} fn callback (optional)\n * @returns {Promise} a list of linked objects\n */\n async findLinkedObjects(obj, type2, field, query, pager, fn) {\n fn = fn || noop;\n fn = checkPager(pager, fn);\n checkParaObject(obj);\n if (!obj || !obj.getId() || !type2) {\n fn([]);\n return resolve([]);\n }\n var params = {\n field: field,\n q: query || '*'\n };\n params = merge(params, this.pagerToParams(pager));\n var url = obj.getObjectURI() + '/links/' + urlEncode(type2);\n var that = this;\n return this.getEntity(this.invokeGet(url, params)).then(function (result) {\n var res = that.getItems(result, pager);\n fn(res);\n return res;\n });\n }\n /**\n * Checks if this object is linked to another.\n * @param {ParaObject} obj the object to execute this method on\n * @param {String} type2 the other type of object\n * @param {String} id2 the other id\n * @param {Function} fn callback (optional)\n * @returns {Promise} true if the two are linked\n */\n async isLinked(obj, type2, id2, fn) {\n fn = fn || noop;\n checkParaObject(obj);\n if (!obj || !obj.getId() || !type2 || !id2) {\n fn(false);\n return resolve(false);\n }\n var url = obj.getObjectURI() + '/links/' + urlEncode(type2) + '/' + urlEncode(id2);\n return this.getEntity(this.invokeGet(url)).then(function (result) {\n var res = result === 'true';\n fn(res);\n return res;\n });\n }\n /**\n * Checks if a given object is linked to this one.\n * @param {ParaObject} obj the object to execute this method on\n * @param {ParaObject} toObj the other object\n * @param {Function} fn callback (optional)\n * @returns {Promise} true if linked\n */\n async isLinkedToObject(obj, toObj, fn) {\n fn = fn || noop;\n checkParaObject(obj);\n checkParaObject(toObj);\n if (!obj || !obj.getId() || !toObj || !toObj.getId()) {\n fn(false);\n return resolve(false);\n }\n return this.isLinked(obj, toObj.getType(), toObj.getId(), fn);\n }\n /**\n * Links an object to this one in a many-to-many relationship.\n * Only a link is created. Objects are left untouched.\n * The type of the second object is automatically determined on read.\n * @param {ParaObject} obj the object to execute this method on\n * @param {String} id2 the other id\n * @param {Function} fn callback (optional)\n * @returns {Promise} the id of the Linker object that is created\n */\n async link(obj, id2, fn) {\n fn = fn || noop;\n checkParaObject(obj);\n if (!obj || !obj.getId() || !id2) {\n fn(null);\n return resolve(null);\n }\n var url = obj.getObjectURI() + '/links/' + urlEncode(id2);\n return this.getEntity(this.invokePost(url), fn);\n }\n /**\n * Unlinks an object from this one.\n * Only a link is deleted. Objects are left untouched.\n * @param {ParaObject} obj the object to execute this method on\n * @param {String} type2 the other type of object\n * @param {String} id2 the other id\n * @param {Function} fn callback (optional)\n * @returns {Promise} promise\n */\n async unlink(obj, type2, id2, fn) {\n fn = fn || no