UNPKG

arrowdbv2demo

Version:
482 lines (432 loc) 17 kB
/** * Performs the REST API calls the ArrowDB Objects. * * @module rest * * @copyright * Copyright (c) 2012-2014 by Appcelerator, Inc. All Rights Reserved. * * @license * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ const ArrowDBError = require('./arrowdbError'), fs = require('fs'), http = require('http'), messages = require('./messages'), request = require('request'), _ = require('lodash'), debug = require('debug')('arrowdb'), fileUploadParams = ['file'], // http/https parameters that file upload uses excludedParameters = ['key', 'pretty_json', 'req', 'res']; // filtered out http/https parameters, that don't need to be in restOptions userLoginParams = ['_login', '_password', '_admin_'] blankMethods = ['create', 'delete', 'get', 'update'] manyMethods = ['createMany', 'deleteMany', 'updateMany'] /* * Public APIs */ module.exports.createArrowDBRequestFunction = createArrowDBRequestFunction; module.exports.createArrowDBRESTRequestFunction = createArrowDBRESTRequestFunction; /** * Internal function for making RESTful calls to ArrowDB API. It will put parameters * including ArrowDB key and `pretty_json` into query string or request body * properly, as well as dealing with the session cookie. * * For required format of ArrowDB API, it will also transform input JSON into correct format. * * @private * * @param {string} apiEntryPoint - The URL to use for API calls. * @param {object} appOptions - An object containing various options. * @param {string} [appOptions.apiEntryPoint] - The URL to use for all requests. * @param {boolean} [appOptions.prettyJson] - When truthy, sets the `pretty_json` REST option. * @param {string} arrowDBMethod - The Method used alongside the main component e.g. create in user/create * @param {string} httpMethod - The HTTP method to use for the request. Valid values include "GET", "POST", "PUT", and "DELETE". * @param {string} cookieString - The cookie to set for the session. * @param {object} restOptions - An object containing all needed parameters per request such as `username` and `password` for the user login request. * @param {function} callback - A function to call after the request completes. */ function arrowDBRequest(apiEntryPoint, appOptions, arrowDBMethod, httpMethod, cookieString, restOptions, callback) { var reqBody = _.omit(restOptions, excludedParameters), theJar = request.jar(), cookie = null; headers = null; // cookie may come from either relayed outside request, or cookieString if (restOptions.req) { if (restOptions.req.headers && restOptions.req.headers.cookie) { cookie = request.cookie(restOptions.req.headers.cookie); cookie && theJar.setCookie(cookie, apiEntryPoint); } if (restOptions.req.headers) { headers = restOptions.req.headers; } // Merge req.query and req.body into parameter JSON if (restOptions.req.query) { reqBody = _.defaults(_.clone(restOptions.req.query), reqBody); } if (restOptions.req.body) { reqBody = _.defaults(_.clone(restOptions.req.body), reqBody); } } // cookieString can from either each requests or ArrowDB instance if (cookieString || restOptions.cookieString) { cookie = request.cookie(restOptions.cookieString || cookieString); cookie && theJar.setCookie(cookie, apiEntryPoint); } // pretty_json can be set as application level from appOptions if (appOptions.prettyJson && !restOptions.hasOwnProperty('pretty_json')) { if (typeof appOptions.prettyJson !== 'boolean') { return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, { typeName: 'prettyJson' })); } reqBody.pretty_json = appOptions.prettyJson; } // response_json_depth can be set as application level from appOptions if (appOptions.responseJsonDepth && !restOptions.hasOwnProperty('response_json_depth')) { if (typeof appOptions.responseJsonDepth !== 'number') { return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, { typeName: 'responseJsonDepth' })); } reqBody.response_json_depth = appOptions.responseJsonDepth; } var requestParam = null, preparedReqBody = {}, hasFile = false; isUserLogin = false; Object.keys(reqBody).forEach(function (item) { var value = reqBody[item]; if (value === undefined || value === null) { // if the value is undefined, just return now so that it's not added to the request return; } else if (fileUploadParams.indexOf(item) !== -1) { hasFile = true; if (typeof value === 'string') { value = fs.createReadStream(value); } } else if (userLoginParams.indexOf(item) !== -1) { isUserLogin = true; } else if (value !== null && typeof value === 'object') { if (apiEntryPoint.includes('/file')) { value = JSON.stringify(value); }else{ preparedReqBody = JSON.stringify(value); } } else if (item == 'data') { preparedReqBody = value; return; } else { value = value.toString(); } preparedReqBody[item] = value; }); if (isUserLogin) { if (arrowDBMethod != "") { var o = {} var arrayFormat o[arrayFormat] = [] o[arrayFormat].push(preparedReqBody); preparedReqBody = JSON.stringify(o[arrayFormat]); }else{ preparedReqBody= JSON.stringify(preparedReqBody); } } requestParam = { url: apiEntryPoint, method: httpMethod, jar: theJar, headers: headers }; if (httpMethod === 'GET') { requestParam.qs = preparedReqBody; } else { // if there is any file upload needed, we need to use formData instead of form if (hasFile) { requestParam.formData = preparedReqBody; } else { requestParam.form = preparedReqBody; } } if (appOptions.proxy) { requestParam.proxy = appOptions.proxy; } debug('request %j',requestParam); request(requestParam, function (error, response, body) { debug('response - error=%o, body=%o',error,body); if (error) { return callback(new ArrowDBError(messages.ERR_REQUEST_FAILED_ERROR, { message: error.toString() })); } if (!response) { return callback(new ArrowDBError(messages.ERR_REQUEST_FAILED_NO_RESPONSE)); } var parsedBody = body; // if the body is a string, try to json parse it if (body && typeof body === 'string') { try { parsedBody = JSON.parse(body); } catch (e) {} } var errmsg = parsedBody !== null && typeof parsedBody === 'object' && parsedBody.meta && parsedBody.meta.message, result = { statusCode: response.statusCode, reason: errmsg || http.STATUS_CODES[response.statusCode] || '', response: response, body: parsedBody }; // if there was no explicit error, yet the request is a 4xx or 5xx then error if (response.statusCode >= 400) { return callback(new ArrowDBError(messages.ERR_REQUEST_UNSUCCESSFUL, result)); } if (restOptions.res) { // if this is a relayed response, we will set response header to include cookie back from ArrowDB API var cookies = theJar.getCookies(apiEntryPoint); restOptions.res.setHeader('Set-Cookie', cookies.join('; ')); } else { result.cookieString = theJar.getCookieString(apiEntryPoint); } callback(null, result); }); } /** * Creates a function for invoking a ArrowDB API. The returned function is * expected to be set in the `ArrowDB` object prototype. * * For example, after creating new instance like: * * var ArrowDB = require('arrowdb'); * var arrowDBApp = new ArrowDB('ARROWDB_APP_KEY'); * * users will have methods like `arrowDBApp.usersLogin()` and `arrowDBApp.likesQuery()`. * * When calling `arrowDBApp.usersLogin()`, internally sdk transforms it to: * * createArrowDBRequestFunction({ * arrowDBObjectKey: 'Users', * arrowDBObjectName: 'users', * arrowDBObjectMethodKey: 'Login', * arrowDBObjectMethodName: 'login', * httpMethod:'POST' * }). * * Then, `createArrowDBRequestFunction()` will return a `function(restOptions, callback)`. * From user side we will get `arrowDBApp.usersLogin(restOptions, callback)`. * * @param {object} options - An object containing various options. * @param {string} options.arrowDBObjectKey - ArrowDB Object key listed under arrowDBObjects/xxx.js, like ACLs, Users, PushNotifications. * @param {string} options.arrowDBObjectName - ArrowDB Object name that is used for entry point composition, like ACLs, users, push_notifications. * @param {string} options.arrowDBObjectMethodKey - The method key of ArrowDB Object listed inner arrowDBObjects/xxx.js, like count, remove, showMe, requestResetPassword. * @param {string} options.arrowDBObjectMethodName - The method name of ArrowDB Object that is used for entry point composition, like count, delete, show/me, request_reset_password. * @param {function} [options.arrowDBObjectMethodPreAction] - An ArrowDB Object method specific internal function to call before making the request. * @param {function} [options.arrowDBObjectMethodPostAction] - An ArrowDB Object method specific internal function to call after a request completes. * @param {string} options.httpMethod - The HTTP method to use for the request. Valid values include "GET", "POST", "PUT", and "DELETE". * * @returns {function(object, function(error, result))} A function that invokes the REST request. */ function createArrowDBRequestFunction(options) { if (!options || !options.arrowDBObjectKey || !options.arrowDBObjectName || !options.httpMethod) { throw new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: 'in createArrowDBRequestFunction' }); } return function (restOptions, callback) { // parameter offset if (typeof restOptions === 'function') {i callback = restOptions; restOptions = null; } if (typeof callback !== 'function') { callback = function () {}; } restOptions || (restOptions = {}); function prepareArrowDBRequest() { // check required app key if (!this.appKey) { return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: 'appKey' })); } if (typeof this.appKey !== 'string') { return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, { typeName: 'app key' })); } // check required app options if (!this.appOptions) { return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: 'appOptions' })); } if (typeof this.appOptions !== 'object') { return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, { typeName: 'app options' })); } // determine the arrowdb method var arrowDBMethod = options.arrowDBObjectMethodName; if (arrowDBMethod !== null && typeof arrowDBMethod === 'object') { var dynamicMethod = arrowDBMethod.entry; if (Array.isArray(arrowDBMethod.variables)) { for (var i = 0, l = arrowDBMethod.variables.length; i < l; i++) { var variable = arrowDBMethod.variables[i]; if (!restOptions[variable]) { return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: variable })); } dynamicMethod = dynamicMethod.replace(variable, restOptions[variable]); } } arrowDBMethod = dynamicMethod; } // recheck if arrowDBMethod is an object and if so, remove unnecessary parameters if (arrowDBMethod !== null && typeof arrowDBMethod === 'object' && Array.isArray(options.arrowDBObjectMethodName.variables)) { options.arrowDBObjectMethodName.variables.forEach(function (variable) { delete restOptions[variable]; }); } // remove arrowDBMethod for BAAS V2 to comply with new API's if (blankMethods.indexOf(arrowDBMethod) !== -1) { arrowDBMethod = "" } // manage arrowDBMethod for BAAS V2 to comply with new API's if (manyMethods.indexOf(arrowDBMethod) !== -1) { arrowDBMethod = arrowDBMethod.replace('Many',''); } slashNeeded = ""; if (arrowDBMethod != ""){ slashNeeded = "/" } var apiEntryPoint = this.appOptions.apiEntryPoint + '/v2/' + options.arrowDBObjectName + slashNeeded + arrowDBMethod // extend to support path overload for predefined calls if (restOptions.path){ apiEntryPoint += '/' + restOptions.path delete restOptions.path } apiEntryPoint += '?key=' + this.appKey; // console.log(apiEntryPoint) // console.log('arrowDBObjectName: %s', options.arrowDBObjectName); // console.log('arrowDBMethod: %s', arrowDBMethod); // console.log('httpMethod: %s', options.httpMethod); // console.log('appKey: %s', this.appKey); // console.log('appOptions: %j', this.appOptions); // console.log('restOptions: %j', restOptions); // console.log('sessionCookieString: %s', this.sessionCookieString); // allow the value to just be a cookie value (as the session) and not a full cookie string if (this.sessionCookieString && this.sessionCookieString.indexOf('=') < 0) { this.sessionCookieString = 'arrowDBSessionID='+encodeURIComponent(this.sessionCookieString); } arrowDBRequest(apiEntryPoint, this.appOptions, arrowDBMethod, options.httpMethod, this.sessionCookieString, restOptions, function (error, result) { if (typeof options.arrowDBObjectMethodPostAction === 'function') { // if the post action has 3 args (err, response, callback), then it's async if (options.arrowDBObjectMethodPostAction.length > 2) { options.arrowDBObjectMethodPostAction.call(this, error, result, function (err, res) { callback(err !== undefined ? err : error, res !== undefined ? res : result); }); return; } options.arrowDBObjectMethodPostAction.call(this, error, result); } callback(error, result); }.bind(this)); } if (typeof options.arrowDBObjectMethodPreAction === 'function') { // if the post action has 2 args (restOptions, callback), then it's async if (options.arrowDBObjectMethodPreAction.length > 1) { options.arrowDBObjectMethodPreAction.call(this, restOptions, function (err) { if (err) { return callback(err); } prepareArrowDBRequest.call(this); }.bind(this)); return; } options.arrowDBObjectMethodPreAction.call(this, options, restOptions); } prepareArrowDBRequest.call(this); }; } /** * Creates a function for invoking a REST call method. The returned function is * expected to be set in the `ArrowDB` object prototype. * * For example, after creating new instance like: * * var ArrowDB = require('arrowdb'); * var arrowDBApp = new ArrowDB('ARROWDB_APP_KEY'); * * users will have methods like `arrowDBApp.get()` and `arrowDBApp.post()`. * * When calling `arrowDBApp.get('/v2/user/login.json')`, internally the SDK * transforms it to: * * createArrowDBRESTRequestFunction({methodPath: '/v2/user/login.json', httpMethod: 'GET'}) * * Then, `createArrowDBRESTRequestFunction()` will return a `function(restOptions, callback)`. * From user side we will get arrowDBApp.get(methodPath, restOptions, callback). * * @param {string} httpMethod - The HTTP method to use for the request. Valid * values include "GET", "POST", "PUT", and "DELETE". * * @returns {function(string, object, function(error result))} A function that * invokes the REST request. */ function createArrowDBRESTRequestFunction(httpMethod) { if (!httpMethod) { throw new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: 'httpMethod' }); } return function (methodPath, restOptions, callback) { // parameter offset if (typeof restOptions === 'function') { callback = restOptions; restOptions = null; } if (typeof callback !== 'function') { callback = function () {}; } restOptions || (restOptions = {}); // check required method path if (!methodPath) { return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: 'methodPath' })); } if (typeof methodPath !== 'string') { return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, { typeName: 'method path' })); } // check required app key if (!this.appKey) { return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: 'appKey' })); } if (typeof this.appKey !== 'string') { return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, { typeName: 'app key' })); } // check required app options if (!this.appOptions) { return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, { parameter: 'appOptions' })); } if (typeof this.appOptions !== 'object') { return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, { typeName: 'app options' })); } keyParam = '?key='; // in case path for REST calls contains already a query paramter if (methodPath.includes('?')) { keyParam = '&key='; } var apiEntryPoint = this.appOptions.apiEntryPoint + methodPath + keyParam + this.appKey; arrowDBRequest(apiEntryPoint, this.appOptions, this.arrowDBMethod, httpMethod, this.sessionCookieString, restOptions, callback); }; }