/** * cloud.js * (high-level AJAX library) * * > This is now part of `parasails`. It was branched from the old "Cloud SDK" * > library at its v1.0.1 -- but from that point on, its versioning has been * > tied to the version of parasails it's bundled in. (All future development * > of Cloud SDK will be as part of parasails.) * * Copyright (c) 2014-present, Mike McNeil * MIT License * * - https://twitter.com/mikermcneil * - https://sailsjs.com/about * - https://sailsjs.com/support * - https://www.npmjs.com/package/parasails * * --------------------------------------------------------------------------------------------- * ## Basic Usage * * Step 1: * * ``` * Cloud.setup({ doSomething: 'POST /api/v1/somethings/:id/do' }); * ``` * ^^Note that this can also be compiled automatically from your Sails app's routes using a script. * * Step 2: * * ``` * var result = await Cloud.doSomething(8); * ``` * * Or: * ``` * var result = await Cloud.doSomething.with({id: 8, foo: ['bar', 'baz']}); * ``` * --------------------------------------------------------------------------------------------- */ (function(factory, exposeUMD){ exposeUMD(this, factory); })(function (_, io, $, SAILS_LOCALS, location, File, FileList, FormData){ // ██████╗ ██████╗ ██╗██╗ ██╗ █████╗ ████████╗███████╗ // ██╔══██╗██╔══██╗██║██║ ██║██╔══██╗╚══██╔══╝██╔════╝ // ██████╔╝██████╔╝██║██║ ██║███████║ ██║ █████╗ // ██╔═══╝ ██╔══██╗██║╚██╗ ██╔╝██╔══██║ ██║ ██╔══╝ // ██║ ██║ ██║██║ ╚████╔╝ ██║ ██║ ██║ ███████╗ // ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // // ██╗ ██╗████████╗██╗██╗ ███████╗ // ██║ ██║╚══██╔══╝██║██║ ██╔════╝ // ██║ ██║ ██║ ██║██║ ███████╗ // ██║ ██║ ██║ ██║██║ ╚════██║ // ╚██████╔╝ ██║ ██║███████╗███████║ // ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ // Module utilities (private) /** * @param {Ref} that * * @throws {Error} If that is not a File instance, a FileList instance, an * array of File instances, a special File wrapper, or an * array of special File wrappers. (Note that, if an array is * provided, this function will only return true if the array * consists of ≥1 item.) */ function _representsOneOrMoreFiles(that) { // FUTURE: add support for Blobs return ( _.isObject(that) && ( (File? that instanceof File : false)|| (FileList? that instanceof FileList : false)|| (_.isArray(that) && that.length > 0 && _.all(that, function(item) { return File? _.isObject(item) && item instanceof File : false; }))|| (File? _.isObject(that) && _.isObject(that.file) && that.file instanceof File : false)|| (_.isArray(that) && that.length > 0 && _.all(that, function(item) { return File? _.isObject(item) && _.isObject(item.file) && item.file instanceof File : false; })) ) ); }//ƒ /** * @param {String} negotiationRule * * @throws {Error} If rule is invalid or absent */ function _verifyErrorNegotiationRule(negotiationRule) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: add support for parley/flaverr/bluebird/lodash-style dictionary negotiation rules // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (_.isNumber(negotiationRule) && Math.floor(negotiationRule) === negotiationRule) { if (negotiationRule > 599 || negotiationRule < 0) { throw new Error('Invalid error negotiation rule: `'+negotiationRule+'`. If a status code is provided, it must be between zero and 599.'); } } else if (_.isString(negotiationRule) && negotiationRule) { // Ok, we'll assume it's fine } else { var suffix = ''; if (negotiationRule === undefined || _.isFunction(negotiationRule)) { suffix = ' Looking to tolerate or intercept **EVERY** error? This usually isn\'t a good idea, because, just like some try/catch usage patterns, it could mean swallowing errors unexpectedly, which can make debugging a nightmare.'; } throw new Error('Invalid error negotiation rule: `'+negotiationRule+'`. Please pass in a valid intercept rule string. An intercept rule is either (A) the name of an exit or (B) a whole number representing the status code like `404` or `200`.'+suffix); } } // ███████╗██╗ ██╗██████╗ ██████╗ ██████╗ ████████╗███████╗ // ██╔════╝╚██╗██╔╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝ // █████╗ ╚███╔╝ ██████╔╝██║ ██║██████╔╝ ██║ ███████╗ // ██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║██╔══██╗ ██║ ╚════██║ // ███████╗██╔╝ ██╗██║ ╚██████╔╝██║ ██║ ██║ ███████║ // ╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // Module exports: /** * Cloud (SDK) * * After setup, this dictionary will have a method for each declared endpoint. * Each key will be a function which sends an HTTP or socket request to a * particular endpoint. * * ### Setup * * ``` * Cloud.setup({ * apiBaseUrl: 'https://example.com', * usageOpts: { * arginStyle: 'serial' * }, * methods: { * doSomething: 'PUT /api/v1/projects/:id', * // ... * } * }); * ``` * * > Note that you should avoid having an endpoint method named "setup", for obvious reasons. * > (Technically, it should work anyway though. But yeah, no reason to tempt the fates.) * * ### Basic Usage * * ``` * var user = await Cloud.findOneUser(3); * ``` * * ``` * var user = await Cloud.findOneUser.with({ id: 3 }); * ``` * * ``` * Cloud.doSomething.with({ * someParam: ['things', 3235, null, true, false, {}, []] * someOtherParam: 2523, * etc: 'more things' * }).exec(function (err, responseBody, responseObjLikeJqXHR) { * if (err) { * // ... * return; * } * * // ... * }); * ``` * * ### Negotiating Errors * ``` * Cloud.signup.with({...}) * .switch({ * error: function (err) { ... }, * usernameAlreadyInUse: function (recommendedAlternativeUsernames) { ... }, * emailAddressAlreadyInUse: function () { ... }, * success: function () { ... } * }); * ``` * * ### Using WebSockets * ``` * Cloud.doSomething.with({...}) * .protocol('jQuery') * .exec(...); * ``` * * ``` * Cloud.doSomething.with({...}) * .protocol('io.socket') * .exec(...); * ``` * * ##### Providing a particular jQuery or SailsSocket instance * * ``` * Cloud.doSomething.with({...}) * .protocol(io.socket) * .exec(...); * ``` * * ``` * Cloud.doSomething.with({...}) * .protocol($) * .exec(...); * ``` * * ### Using Custom Headers * ``` * Cloud.doSomething.with({...}) * .headers({ * 'X-Auth': 'whatever' * }) * .exec(...); * ``` * * ### CSRF Protection * * It `SAILS_LOCALS._csrf` is defined, then it will be sent * as the "x-csrf-token" header for all Cloud.* requests, automatically. * */ var Cloud = {}; // FUTURE: Cloud.getUrlFor() // (similar to https://sailsjs.com/documentation/reference/application/sails-get-url-for) // (but would def need to provide a way of providing values for URL pattern variables like `:id`) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: finish this when time allows (might be better to have it work by attaching dedicated // nav methods rather than a generic nav method though) // ``` // // A mapping of names of view actions to URL // // > provided to `.setup()`, for use in .navigate() // var _navigableUrlsByViewActionName; // // // /** // * Cloud.navigate() // * // * Call this function to navigate to a different web page. // * (Be sure and call it *before* trying to use any of the endpoint methods!) // * // * @param {String} destination // * A URL or the name of a view action. // */ // Cloud.navigate = function(destination) { // var doesBeginWithSlash = _.isString(destination) && destination.match(/^\//); // var doesBeginWithHttp = _.isString(destination) && destination.match(/^http/); // var isProbablyTheNameOfAViewAction = _.isString(destination) && destination.match(/^view/); // if (!_.isString(destination) || !(doesBeginWithSlash || doesBeginWithHttp || isProbablyTheNameOfAViewAction)) { // throw new Error('Bad usage: Cloud.navigate() should be called with a URL or the name of a view action.'); // } // if (!_navigableUrlsByViewActionName) { // throw new Error('Cannot navigate to a view action because Cloud.setup() has not been called yet-- please do that first (or if that\'s not possible, just navigate directly to the URL)'); // } // }; // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Cloud.setup() * * Call this function once, when the page loads. * (Be sure and call it *before* trying to use any of the endpoint methods!) * * @param {Dictionary} options * @required {Dictionary} methods * @optional {Dictionary} links * @optional {Dictionary} apiBaseUrl */ Cloud.setup = function(options) { options = options || {}; if (!_.isObject(options.methods) || _.isArray(options.methods) || _.isFunction(options.methods)) { throw new Error('Cannot .setup() Cloud SDK: `methods` must be provided as a dictionary of addresses and definitions.'); }//• // Determine the proper API base URL if (!options.apiBaseUrl) { if (location) { options.apiBaseUrl = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''); } else { throw new Error('Cannot .setup() Cloud SDK: Since a location cannot be determined, `apiBaseUrl` must be provided as a string (e.g. "https://example.com").'); } }//fi // Apply the base URL for the benefit of WebSockets (if relevant): if (io) { io.sails.url = options.apiBaseUrl; }//fi // The name of the default protocol. var DEFAULT_PROTOCOL_NAME = 'jQuery'; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: finish this when time allows (would be better to have it work by attaching dedicated // nav methods rather than a generic nav method though) // ``` // // Save a reference to the mapping of navigable URLs by view action name (if provided). // _navigableUrlsByViewActionName = options.links || {}; // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (options.methods.on) { throw new Error('Cannot .setup() Cloud SDK: `.on()` is reserved. It cannot be used as the name for a method.'); } if (options.methods.off) { throw new Error('Cannot .setup() Cloud SDK: `.off()` is reserved. It cannot be used as the name for a method.'); } // Interpret methods var methods = _.reduce(options.methods, function(memo, appLevelSdkEndpointDef, methodName) { if (methodName === 'setup') { console.warn('"setup" is a confusing name for a cloud action (it conflicts with a built-in feature of this SDK itself). Would "initialize()" work instead? (Continuing this time...)'); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: finish this when time allows (would be better to have it work by attaching dedicated // nav methods rather than a generic nav method though) // ``` // if (methodName === 'navigate') { // console.warn('"navigate" is a confusing name for a cloud action (it conflicts with a built-in feature of this SDK itself). Would "travel()" work instead? (Continuing this time...)'); // } // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Validate the endpoint definition. //////////////////////////////////////////////////////////////////////////////////////////////// var _verbToCheck; var _urlToCheck; if (typeof appLevelSdkEndpointDef === 'function') { // We can't really check functions, so we just let it through. } else { if (appLevelSdkEndpointDef && typeof appLevelSdkEndpointDef === 'object') { // Must have `verb` and `url` properties. _verbToCheck = appLevelSdkEndpointDef.verb; _urlToCheck = appLevelSdkEndpointDef.url; } else if (typeof appLevelSdkEndpointDef === 'string') { // Must be able to parse `verb` and `url`. _verbToCheck = appLevelSdkEndpointDef.replace(/^\s*([^\/\s]+)\s*\/.*$/, '$1'); _urlToCheck = appLevelSdkEndpointDef.replace(/^\s*[^\/\s]+\s*\/(.*)$/, '/$1'); } else { throw new Error('CloudSDK endpoint (`'+methodName+'`) is invalid: Endpoints should be defined as either (1) a string like "GET /foo", (2) a dictionary containing a `verb` and a `url`, or (3) a function that returns a dictionary like that.'); } // --• // `verb` must be valid. if (typeof _verbToCheck !== 'string' || _verbToCheck === '') { throw new Error('CloudSDK endpoint (`'+methodName+'`) is invalid: An endpoint\'s `verb` should be defined as a non-empty string.'); } // `url` must be valid. if (typeof _urlToCheck !== 'string' || _urlToCheck === '') { throw new Error('CloudSDK endpoint (`'+methodName+'`) is invalid: An endpoint\'s `url` should be defined as a non-empty string.'); } } // Build the actual method that will be called at runtime: //////////////////////////////////////////////////////////////////////// var _helpCallCloudMethod = function (argins) { //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++// // There are 3 ways to define an SDK wrapper for a cloud endpoint. //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++// var requestInfo = { // • the HTTP verb (aka HTTP "method" -- we're just using "verb" for clarity) verb: undefined, // • the path part of the URL url: undefined, // • a dictionary of request data // (depending on the circumstances, these params will be encoded directly // into either the url path, the querystring, or the request body) params: undefined, // • a dictionary of custom request headers headers: undefined, // • the protocol name (e.g. "jQuery" or "io.socket") protocolName: undefined, // • the protocol instance (e.g. actual reference to `$` or `io.socket`) protocolInstance: undefined, // • an array of conditional lifecycle instructions from userland .intercept() / .tolerate() calls, if any are configured lifecycleInstructions: [], }; // ██████╗ ██╗ ██╗██╗██╗ ██████╗ ██████╗ ███████╗███████╗███████╗██████╗ ██████╗ ███████╗██████╗ // ██╔══██╗██║ ██║██║██║ ██╔══██╗ ██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██╔══██╗ // ██████╔╝██║ ██║██║██║ ██║ ██║ ██║ ██║█████╗ █████╗ █████╗ ██████╔╝██████╔╝█████╗ ██║ ██║ // ██╔══██╗██║ ██║██║██║ ██║ ██║ ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██╔══██╗██╔══██╗██╔══╝ ██║ ██║ // ██████╔╝╚██████╔╝██║███████╗██████╔╝ ██████╔╝███████╗██║ ███████╗██║ ██║██║ ██║███████╗██████╔╝ // ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ // // ██████╗ ██████╗ ██╗███████╗ ██████╗████████╗ // ██╔═══██╗██╔══██╗ ██║██╔════╝██╔════╝╚══██╔══╝ // ██║ ██║██████╔╝ ██║█████╗ ██║ ██║ // ██║ ██║██╔══██╗██ ██║██╔══╝ ██║ ██║ // ╚██████╔╝██████╔╝╚█████╔╝███████╗╚██████╗ ██║ // ╚═════╝ ╚═════╝ ╚════╝ ╚══════╝ ╚═════╝ ╚═╝ // // Used for avoiding accidentally creating multiple promises when // using .then() or .catch(). var _promise; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: add support for omens so we get better stack traces, particularly // when running this in a Node.js environment. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Return a dictionary of functions (to allow for "deferred object" usage.) var deferred = { // Allow request headers to be configured. ///////////////////////////////////////////////////////////////////////////// headers: function (_customRequestHeaders){ if (!_.isObject(_customRequestHeaders)) { throw new Error('Invalid request headers: Must be specified as a dictionary, where each key has a string value.'); } requestInfo.headers = _.extend(requestInfo.headers||{}, _customRequestHeaders); return deferred; }, // Allow the protocol to be configured on a per-request basis. ///////////////////////////////////////////////////////////////////////////// protocol: function (_protocolNameOrInstance){ if (typeof _protocolNameOrInstance === 'string') { switch (_protocolNameOrInstance) { case 'jQuery': requestInfo.protocolName = 'jQuery'; if ($ === undefined) { throw new Error('Could not access jQuery: `$` is undefined.'); } else { requestInfo.protocolInstance = $; } break; case 'io.socket': requestInfo.protocolName = 'io.socket'; if (typeof io === 'undefined') { throw new Error('Could not access `io.socket`: `io` is undefined.'); } else if (typeof io !== 'function') { throw new Error('Could not access `io.socket`: `io` is invalid:' + io); } else if (typeof io.socket === 'undefined') { throw new Error('Could not access `io.socket`: `io` does not have a `socket` property. Make sure `sails.io.js` is being injected in a