12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745 |
- /**
- * cloud.js
- * (high-level AJAX library)
- *
- * > This is now part of parasails. Originally branched from the original
- * > Cloud SDK library at v1.0.1. (All future development of Cloud SDK will be
- * > as part of parasails.)
- *
- * Copyright (c) 2015-2017, Mike McNeil, Scott Gress, Sails Co. (https://sailsjs.com/about)
- * Copyright (c) 2014, Mike McNeil, Balderdash Design Co. (http://balderdash.co)
- * MIT License
- * ---------------------------------------------------------------------------------------------
- * ## 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(global, factory){
- var _;
- var io;
- var $;
- var SAILS_LOCALS;
- var location;
- var File;
- var FormData;
- // First, handle optional deps that are gleaned from the global state:
- // > Note: Instead of throwing, we ignore invalid globals.
- // > (Remember the bug w/ the File global that happened in Socket.io
- // > back in ~2015!)
- // =====================================================================
- if (global.location !== undefined) {
- if (global.location && typeof global.location === 'object' && (global.location.constructor.name === 'Location' || global.location.constructor.toString() === '[object Location]')) {
- location = global.location;
- }
- }//fi
- if (global.File !== undefined) {
- if (global.File && typeof global.File === 'function' && global.File.name === 'File') {
- File = global.File;
- }
- }//fi
- if (global.FormData !== undefined) {
- if (global.FormData && typeof global.FormData === 'function' && global.FormData.name === 'FormData') {
- FormData = global.FormData;
- }
- }//fi
- // Then, load the rest of the deps:
- // =====================================================================
- //˙°˚°·.
- //‡CJS ˚°˚°·˛
- if (typeof exports === 'object' && typeof module !== 'undefined') {
- var _require = require;// eslint-disable-line no-undef
- var _module = module;// eslint-disable-line no-undef
- // required deps:
- if (typeof _ === 'undefined') {
- try {
- _ = _require('@sailshq/lodash');
- } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
- }//fi
- if (typeof _ === 'undefined') {
- try {
- _ = _require('lodash');
- } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
- }//fi
- // optional deps:
- try { $ = _require('jquery'); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
- try {
- io = _require('socket.io-client');
- var sailsIO = _require('sails.io.js');
- // Instantiate the library (and start auto-connecting)
- io = sailsIO(io);
- // Disable logging
- io.sails.environment = 'production';
- // Note that, if there is no location global, then after one tick,
- // if `io.sails.url` has still not been set, weird errors will emerge.
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: figure out a way to provide a better err msg about this--
- // i.e. specifically the case where `.setup()` isn't called within one tick.
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
- SAILS_LOCALS = undefined;
- // export:
- _module.exports = factory(_, io, $, SAILS_LOCALS, location, File, FormData);
- }
- //˙°˚°·
- //‡AMD ˚¸
- else if(typeof define === 'function' && define.amd) {// eslint-disable-line no-undef
- throw new Error('Global `define()` function detected, but built-in AMD support in `cloud.js` is not currently recommended. To resolve this, modify `cloud.js`.');
- // var _define = define;// eslint-disable-line no-undef
- // _define(['_', 'sails.io.js', '$', 'SAILS_LOCALS', 'location', 'file'], factory);
- }
- //˙°˚˙°·
- //‡NUDE ˚°·˛
- else {
- // required deps:
- if (!global._) { throw new Error('`_` global does not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the Lodash library is getting brought in before `cloud`.)'); }
- _ = global._;
- // optional deps:
- if (global.io !== undefined) {
- if (typeof global.io !== 'function') {
- throw new Error('Could not access `io.socket`: The `io` global is invalid at the moment:' + global.io + '\n(If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the sails.io.js library is getting brought in before `cloud`.)');
- }
- else if (typeof global.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 <script> tag!');
- }
- else {
- io = global.io;
- }
- }//fi
- if (global.$ !== undefined) {
- if (typeof global.$ !== 'function') {
- throw new Error('The `$` global is not valid at the moment:' + global.$ + '\n(If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the jQuery library is getting brought in before `cloud`.)');
- }
- else {
- $ = global.$;
- }
- }//fi
- if (global.SAILS_LOCALS !== undefined) {
- if (!_.isObject(global.SAILS_LOCALS)) {
- throw new Error('The `SAILS_LOCALS` global is not valid at the moment:' + global.SAILS_LOCALS + '\n(Please check and make sure you are using `<%- exposeLocalsToBrowser() %>` in your server-side view *before* the rest of your scripts.)');
- }
- else {
- SAILS_LOCALS = global.SAILS_LOCALS;
- }
- }//fi
- // export:
- if (global.Cloud) { throw new Error('Cannot expose global variable: Conflicting global (`cloud`) already exists!'); }
- global.Cloud = factory(_, io, $, SAILS_LOCALS, location, File, FormData);
- }
- })(this, function (_, io, $, SAILS_LOCALS, location, File, FormData){
- /**
- * @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);
- }
- }
- /**
- * 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: finish this when time allows (would 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 || {};
- // ```
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // 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 <script> tag!');
- }
- else {
- requestInfo.protocolInstance = io.socket;
- }
- break;
- default:
- throw new Error('Unrecognized protocol: `'+_protocolNameOrInstance+'`. Use "jQuery" or "io.socket".');
- }
- }
- else if (_.isObject(_protocolNameOrInstance) || _.isFunction(_protocolNameOrInstance)) {
- if (_protocolNameOrInstance.name === 'jQuery') {
- requestInfo.protocolName = 'jQuery';
- requestInfo.protocolInstance = _protocolNameOrInstance;
- }
- else if (_protocolNameOrInstance.constructor.name === 'SailsSocket') {
- requestInfo.protocolName = 'io.socket';
- requestInfo.protocolInstance = _protocolNameOrInstance;
- }
- else if (_protocolNameOrInstance.toString() === '[Package: machinepack-http]') {
- requestInfo.protocolName = 'machinepack-http';
- requestInfo.protocolInstance = _protocolNameOrInstance;
- }
- // FUTURE: maybe "axios"?
- // FUTURE: maybe "fetch"?
- // FUTURE: maybe "request"?
- else {
- throw new Error('Unrecognized instance provided to `.protocol()`: `'+_protocolNameOrInstance+'`');
- }
- }
- else {
- throw new Error('Unrecognized protocol: `'+_protocolNameOrInstance+'`. Use "jQuery" or "io.socket".');
- }
- return deferred;
- },//</ implementation of `.protocol()`>
- // Allow intercepting the response before resolution/rejection occurs.
- // (This is basically an "after receiving response" lifecycle callback.)
- /////////////////////////////////////////////////////////////////////////////
- intercept: function (negotiationRule, handler) {
- _verifyErrorNegotiationRule(negotiationRule);
- if (!_.isFunction(handler)) {
- throw new Error('Invalid 2nd argument to `.intercept()`. Expecting a handler function.');
- }
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: Add a best-effort check to make sure there is no pre-existing rule
- // that matches this one (i.e. already previously registered using .tolerate()
- // or .intercept())
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- requestInfo.lifecycleInstructions.push({
- type: 'intercept',
- rule: negotiationRule,
- handler: handler
- });
- return deferred;
- },
- // Allow explicitly tolerating certain kinds of responses before resolution/rejection occurs.
- // (This causes control flow convergence by using `.intercept()` + throwing a special value)
- /////////////////////////////////////////////////////////////////////////////
- tolerate: function (_negotiationRuleMaybe, _handlerMaybe) {
- var handler;
- var negotiationRule;
- if (_handlerMaybe === undefined && _.isFunction(_negotiationRuleMaybe)) {
- handler = _negotiationRuleMaybe;
- }
- else {
- negotiationRule = _negotiationRuleMaybe;
- handler = _handlerMaybe;
- }
- if (negotiationRule !== undefined) {
- _verifyErrorNegotiationRule(negotiationRule);
- }
- if (handler !== undefined && !_.isFunction(handler)) {
- throw new Error('Invalid 2nd argument. to `.tolerate()`. Expecting a handler function.');
- }
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: Add a best-effort check to make sure there is no pre-existing rule
- // that matches this one (i.e. already previously registered using .tolerate()
- // or .intercept())
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- requestInfo.lifecycleInstructions.push({
- type: 'tolerate',
- rule: negotiationRule,
- handler: handler?
- handler
- :
- function(){ return; }
- });
- return deferred;
- },
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // Looking for the EarlyReturnSignal stuff?
- //
- // See https://stackoverflow.com/a/43402123/486547 and specifically also
- // https://stackoverflow.com/questions/29499582/how-to-properly-break-out-of-a-promise-chain#comment80446341_43402123
- // (there may be a way to do this more elegantly without requiring the calling
- // code environment to be aware of our special Errors-- but it's not worth it
- // as-is. Too much black magic!)
- //
- // > More notes & background leading up to this:
- // > https://gist.github.com/mikermcneil/c1bc2d57f5bedae810295e5ed8c5f935
- // >
- // > (Also check out the commit history of the original `caviar` repo.)
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- then: function (){
- // console.log('in implementation of `then()`...');
- var promise = deferred.toPromise();
- // console.log('obj:',promise);
- return promise.then.apply(promise, arguments);
- },
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: use parley for all this instead, if we can find a way to keep it
- // from being too enormous when browserified
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- toPromise: function (){
- if (typeof Promise === 'undefined') { throw new Error('Cannot use this approach: `Promise` constructor not available in current environment.'); }
- if (_promise) {
- // console.log('using catched promise...');
- return _promise;
- }
- // console.log('instantiating new promise!');
- _promise = new Promise(function(resolve, reject){// eslint-disable-line no-undef
- try {
- deferred.exec(function(err, resultMaybe) {
- if (err){
- // console.log('calling reject..');
- return reject(err);
- }
- // console.log('calling resolve..');
- return resolve(resultMaybe);
- });//_∏_
- } catch (err) {
- // console.log('EXEC THREW ERROR!',err);
- // console.log('CALLED REJECT IN NATIVE CATCH BLOCK!');
- reject(err);
- }
- });//_∏_
- return _promise;
- },
- // Allow the AJAX request to actually be sent.
- /////////////////////////////////////////////////////////////////////////////
- exec: function (exitCallbacks){
- if (exitCallbacks) {
- if (!_.isObject(exitCallbacks) && !_.isFunction(exitCallbacks)) {
- throw new Error('If specified, the argument passed to `.exec()` must be a dictionary containing a `success` and `error` callback. Alternatively, you can use a Node.js-style callback.');
- }
- else if (_.isObject(exitCallbacks) && exitCallbacks.success && !_.isFunction(exitCallbacks.success)) {
- throw new Error('If specified, `success` callback must be a function.');
- }
- else if (_.isObject(exitCallbacks) && exitCallbacks.error && !_.isFunction(exitCallbacks.error)) {
- throw new Error('If specified, `error` callback must be a function.');
- }
- }
- // Just in case, build an error instance beforehand.
- // (This ensures it has a good stack trace.)
- var errorInstance = new Error('Endpoint (`'+methodName+'`) responded with an error (or the request failed).');
- // Give the error a special `name` property to ease negotiation
- // (vs. other unrelated things like typos in argins)
- errorInstance.name = 'CloudError';
- // If present, use CSRF token from `SAILS_LOCALS` as the `x-csrf-token`
- // request header for all non-GET requests.
- // (Unless of course there's another x-csrf-token header already specified.)
- if (_.isObject(SAILS_LOCALS) && typeof SAILS_LOCALS._csrf !== 'undefined') {
- if (_.isUndefined(requestInfo.headers)) {
- requestInfo.headers = {};
- }// >-
- if (!requestInfo.headers['x-csrf-token']) {
- requestInfo.headers['x-csrf-token'] = SAILS_LOCALS._csrf;
- }
- }//fi
- // Finally, use the appropriate protocol to actually send the request and
- // send back the response to the code that called this `Cloud.*()` method.
- (function _makeAjaxCallWithAppropriateProtocol(proceed){
- // First, tease apart text params and file params.
- var textParamsByFieldName = requestInfo.params;
- // Check for file uploads.
- //
- // If `File`+`FormData` constructors are available, check to
- // see if any of the param values are File instances. If
- // they are, then remove them from a shallow clone of the
- // params dictionary, and set them up separately.
- // (The files will be attached to the request _after_
- // the text parameters.)
- var filesByFieldName = {};
- if (File && FormData && textParamsByFieldName) {
- textParamsByFieldName = _.extend({}, textParamsByFieldName);
- _.each(textParamsByFieldName, function(value, fieldName){
- if (_.isObject(value) && value instanceof File) {
- filesByFieldName[fieldName] = value;
- delete textParamsByFieldName[fieldName];
- }
- });
- }//fi
- // Don't allow file uploads for GET requests,
- // or if the FormData constructor is somehow missing.
- if (_.keys(filesByFieldName).length > 0) {
- if (requestInfo.verb.match(/get/i)) {
- throw new Error(
- 'Detected File instance(s) provided for parameter(s): '+
- _.keys(filesByFieldName)+'\n'+
- 'But this is a nullipotent ('+requestInfo.verb.toUpperCase()+') '+
- 'request, which does not support file uploads.'
- );
- }//•
- if (!FormData) {
- throw new Error(
- 'Detected File instance(s) provided for parameter(s): '+
- _.keys(filesByFieldName)+'\n'+
- 'But the native FormData constructor does not exist!'
- );
- }
- }//fi
- switch (requestInfo.protocolName) {
- // ▄▄███▄▄· █████╗ ██╗ █████╗ ██╗ ██╗ ██╗██╗
- // ██╔════╝ ██╔══██╗ ██║██╔══██╗╚██╗██╔╝██╔╝╚██╗
- // ███████╗ ███████║ ██║███████║ ╚███╔╝ ██║ ██║
- // ╚════██║ ██╔══██║██ ██║██╔══██║ ██╔██╗ ██║ ██║
- // ███████║██╗██║ ██║╚█████╔╝██║ ██║██╔╝ ██╗╚██╗██╔╝
- // ╚═▀▀▀══╝╚═╝╚═╝ ╚═╝ ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝
- case 'jQuery': return (function _doAjaxWithJQuery(){
- var thisJQuery = requestInfo.protocolInstance;
- // Build options for $.ajax().
- var ajaxOpts = {
- url: requestInfo.url,
- method: requestInfo.verb
- };
- // If GET request, encode params in querystring.
- if (requestInfo.verb.match(/get/i)) {
- ajaxOpts.data = textParamsByFieldName;
- }
- // Else if there are files, attach them properly,
- // alongside the other stuff in the form.
- // > Note that we include text params **FIRST**,
- // > in order to support order-aware body parsers
- // > that rely on pessimistic upstream awareness
- // > optimize uploads and prevent DDoS attacks.
- else if (_.keys(filesByFieldName).length > 0){
- ajaxOpts.processData = false;
- ajaxOpts.contentType = false;
- ajaxOpts.data = new FormData();
- _.each(textParamsByFieldName, function(value, fieldName){
- // Skip `undefined` values to more accurately mirror
- // the behavior of JSON.stringify()
- if (value === undefined) { return; }
- ajaxOpts.data.append(fieldName, value);
- });
- _.each(filesByFieldName, function(file, fieldName){
- // Skip `undefined` values for consistency.
- if (file === undefined) { return; }
- if (!_.isObject(file) || !_.isObject(file.constructor) || file.constructor.name !== 'File') {
- throw new Error('Cannot upload as '+fieldName+' because the provided value is not a File instance. Instead, got:'+file);
- }
- ajaxOpts.data.append(fieldName, file, file.name);
- });
- }
- // Otherwise, attach params as a JSON-encoded request body.
- else {
- ajaxOpts.data = JSON.stringify(textParamsByFieldName);
- ajaxOpts.processData = false;
- ajaxOpts.contentType = 'application/json; charset=UTF-8';
- }
- if (typeof requestInfo.headers !== 'undefined') {
- ajaxOpts.headers = requestInfo.headers;
- }
- // Dealing with jqXHR:
- //
- // To get status code:
- // console.log(jqXHR.statusCode);
- //
- // To get header(s):
- // console.log(jqXHR.getResponseHeader('foo'));
- // - or -
- // console.log(jqXHR.getAllResponseHeaders());
- // ^^^ but this one gives it to you as a string.
- // ^^
- // WARNING: if using a cross-domain request w/ CORS, this (^^^^^)
- // header grabbing may not work properly on some versions of firefox. More details:
- // http://stackoverflow.com/questions/5614735/jqxhr-getallresponseheaders-wont-return-all-headers
- thisJQuery.ajax(_.extend(ajaxOpts, {
- error: function (jqXHR) {
- return proceed(undefined, {
- body: jqXHR.responseJSON === undefined ? jqXHR.responseText : jqXHR.responseJSON,
- statusCode: jqXHR.status,
- headers: _.reduce(jqXHR.getAllResponseHeaders().split(/\n/), function (memo, pair) {
- var splitPair = pair.split(/:/);
- var headerName = splitPair[0];
- if (headerName === '') { return memo; }
- // Note that we trim leading AND trailing whitespace.
- var headerVal = splitPair.slice(1).join('').replace(/^\s*/, '').replace(/\s*$/, '');
- memo[headerName] = headerVal;
- // Also add an alias using the all-lowercased version of the header name
- // (if it's different)
- var allLowercaseHeaderName = headerName.toLowerCase();
- if (allLowercaseHeaderName !== headerName) {
- memo[allLowercaseHeaderName] = headerVal;
- }
- return memo;
- }, {})
- });
- },
- success: function (unused0, unused1, jqXHR) {
- return proceed(undefined, {
- body: jqXHR.responseJSON === undefined ? jqXHR.responseText : jqXHR.responseJSON,
- statusCode: jqXHR.status,
- headers: _.reduce(jqXHR.getAllResponseHeaders().split(/\n/), function (memo, pair) {
- var splitPair = pair.split(/:/);
- var headerName = splitPair[0];
- if (headerName === '') { return memo; }
- // Note that we trim leading AND trailing whitespace.
- var headerVal = splitPair.slice(1).join('').replace(/^\s*/, '').replace(/\s*$/, '');
- memo[headerName] = headerVal;
- // Also add an alias using the all-lowercased version of the header name
- // (if it's different)
- var allLowercaseHeaderName = headerName.toLowerCase();
- if (allLowercaseHeaderName !== headerName) {
- memo[allLowercaseHeaderName] = headerVal;
- }
- return memo;
- }, {})
- });
- }
- }));//</ thisJQuery.ajax + _.extend() >
- })();//</self-calling function :: _doAjaxWithJQuery>
- // ██╗ ██████╗ ███████╗ ██████╗ ██████╗██╗ ██╗███████╗████████╗ ██╗██╗
- // ██║██╔═══██╗ ██╔════╝██╔═══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝▄ ██╗▄██╔╝╚██╗
- // ██║██║ ██║ ███████╗██║ ██║██║ █████╔╝ █████╗ ██║ ████╗██║ ██║
- // ██║██║ ██║ ╚════██║██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ▀╚██╔▀██║ ██║
- // ██║╚██████╔╝██╗███████║╚██████╔╝╚██████╗██║ ██╗███████╗ ██║██╗ ╚═╝ ╚██╗██╔╝
- // ╚═╝ ╚═════╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝╚═╝ ╚═╝╚═╝
- //
- case 'io.socket': return (function _doAjaxWithSocket(){
- var socket = requestInfo.protocolInstance;
- // If `File` constructor is available, check to be sure
- // that none of the parameter values are File instances.
- // > Note that if the File constructor is NOT available,
- // > then we don't even bother checking (it's not like it
- // > would work anyway!)
- if (File && requestInfo.params) {
- _.each(requestInfo.params, function(value, fieldName){
- if (_.isObject(value) && value instanceof File) {
- throw new Error('Detected File instance provided for the `'+fieldName+'` parameter -- but file uploads are not currently supported using WebSockets / Socket.io. Please call this method using a different request protocol (e.g. `protocol: \'jQuery\'`)');
- }
- });
- }//fi
- // Determine if the socket has been disconnected, or if it
- // has NEVER BEEN connected and is not CURRENTLY TRYING to
- // connect.
- var disconnectedOrWasNeverConnectedAndUnlikelyToTry =
- // =>
- // If the socket is connected, cool, no problem.
- !socket.isConnected() &&
- // =>
- // If the socket is at least _attempting_ to connect, we'll go ahead
- // and let it try to do it's thing (i.e. queue and replay)
- !socket.isConnecting() &&
- // =>
- // If the socket hasn't even had the _chance_ to begin connecting
- // (because the one-tick auto-connect timer hasn't fired yet),
- // then we'll give it that chance.
- !socket.mightBeAboutToAutoConnect();
- // If none of the above were true, then emulate a normal
- // offline AJAX response from jQuery.
- if (disconnectedOrWasNeverConnectedAndUnlikelyToTry) {
- return proceed(undefined, {
- body: null,
- statusCode: 0,
- headers: {}
- });
- }
- // Otherwise the socket is either connected, in the process of connecting,
- // or in an indeterminate state where it has _never_ connected but _might_
- // still connect (see above for details).
- //
- // In any of these cases, thanks largely to queuing, it is safe to continue
- // onwards, and to send the request!
- socket.request({
- method: requestInfo.verb,
- url: requestInfo.url,
- data: requestInfo.params,
- headers: requestInfo.headers
- }, function (unused, jwres) {
- return proceed(undefined, {
- body: jwres.body,
- statusCode: jwres.statusCode,
- headers: jwres.headers
- });
- });//</ socket.request() >
- })();//</self-calling function :: _doAjaxWithSocket>
- // ███╗ ███╗██████╗ ██╗ ██╗████████╗████████╗██████╗
- // ████╗ ████║██╔══██╗ ██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗
- // ██╔████╔██║██████╔╝█████╗███████║ ██║ ██║ ██████╔╝
- // ██║╚██╔╝██║██╔═══╝ ╚════╝██╔══██║ ██║ ██║ ██╔═══╝
- // ██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║ ██║
- // ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
- //
- case 'machinepack-http': return (function _doAjaxWithMpHttp(){
- // If `File` constructor is available, check to be sure
- // that none of the parameter values are File instances.
- // > Note that if the File constructor is NOT available,
- // > then we don't even bother checking (it's not like it
- // > would work anyway!)
- if (File && requestInfo.params) {
- _.each(requestInfo.params, function(value, fieldName){
- if (_.isObject(value) && value instanceof File) {
- throw new Error('Detected File instance provided for the `'+fieldName+'` parameter -- but file uploads are not currently supported using machinepack-http. Please call this method using a different request protocol.');
- }
- });
- }//fi
- var mpHttpOpts = {
- url: requestInfo.url,
- method: requestInfo.verb
- };
- // If GET request, encode params in querystring.
- if (requestInfo.verb.match(/get/i)) {
- mpHttpOpts.qs = textParamsByFieldName;
- }
- // Otherwise, attach params as the request body.
- // (it will be JSON-encoded automatically by default)
- else {
- mpHttpOpts.body = textParamsByFieldName;
- }
- if (typeof requestInfo.headers !== 'undefined') {
- mpHttpOpts.headers = requestInfo.headers;
- }
- requestInfo.protocolInstance.sendHttpRequest(mpHttpOpts)
- .switch({
- error: function (err) {
- return proceed(err);
- },
- requestFailed: function(err) {
- return proceed(undefined, {
- body: err.message,
- statusCode: 0,
- headers: {}
- });
- },
- non200Response: function(serverResponse) {
- return proceed(undefined, serverResponse);
- },
- success: function (serverResponse){
- // If there is no response body (i.e. `body` is `""`),
- // then we'll interpret that as `null` and return that as
- // our response data.
- if (serverResponse.body === '') {
- serverResponse.body = null;
- }
- // --•
- // Otherwise, attempt to parse the response body as JSON.
- try {
- serverResponse.body = JSON.parse(serverResponse.body);
- } catch (err) {//eslint-disable-line no-unused-vars
- // If the raw response body string cannot be parsed as JSON,
- // then interpret it as a string by leaving the raw body as-is.
- }
- return proceed(undefined, serverResponse);
- }
- });//_∏_
- })();//</self-calling function :: _doAjaxWithMpHttp>
- default:
- throw new Error('Consistency violation: Unexpected protocol name received (`'+requestInfo.protocolName+'`)-- but it should have already been checked!');
- }//</switch(protocol)>
- })(function afterwards(err, responseInfo){
- if (err) {
- throw new Error('Consistency violation: Unexpected error in CloudSDK. Details: '+err.stack);
- }
- // Note that the response info dictionary is intended to be
- // a transport-agnostic way of representing a server response.
- // (similar to jQuery AJAX response objects / jqXHR / jwRes):
- // --------------------------------------------------------------------
- // AVAILABLE AT THIS POINT:
- // • responseInfo.statusCode
- // • responseInfo.body
- // • responseInfo.headers
- // --------------------------------------------------------------------
- // ATTACHED BELOW:
- // • responseInfo.data << for compatibility
- // • responseInfo.exit << either `success`, `error`, or the code name of some other exit.
- // • responseInfo.code << (alias for "exit")
- // --------------------------------------------------------------------
- // To get exit info:
- // console.log(responseInfo.headers['X-Exit']);
- // console.log(responseInfo.headers['X-Exit-FriendlyName']);
- // console.log(responseInfo.headers['X-Exit-Description']);
- // console.log(responseInfo.headers['X-Exit-Extended-Description']);
- // console.log(responseInfo.headers['X-Exit-Output-Friendly-Name']);
- // console.log(responseInfo.headers['X-Exit-Output-Description']);
- // COMPATIBILITY:
- // Stick `data` property on responseInfo so it feels familiar
- // e.g. like `res.data` in angular 1
- // (This is mainly for backwards compatibility, and can probably
- // be removed at some point.)
- if (!_.isUndefined(responseInfo.body)) {
- responseInfo.data = responseInfo.body;
- }
- // Determine the appropriate callback to call.
- //
- // We also stick on `exit` property for convenience
- // by sniffing the X-Exit header. We default to `error`
- // or `success`, depending on whether an Error instance
- // was passed through as the `error` property.
- var xExitResponseHeaderValue = responseInfo.headers['x-exit'] || responseInfo.headers['X-Exit'];
- // ^This "either-or-ing" is likely necessary because of different jQuery versions.
- if (xExitResponseHeaderValue === '_offline') {
- console.warn('Unconventional exit detected: `_offline` is a reserved exit name for use on the front-end, and should not be used willy nilly. Instead, please come up with a different exit name for this scenario.');
- }//fi
- // If the user's computer is offline or the server is down, etc...
- // > If `statusCode` is 0, then the user is probably offline.
- // > Or maybe our server is down omg.
- // > Or it could be that a cross-origin request was blocked.
- if (responseInfo.statusCode === 0) {
- responseInfo.exit = '_offline';
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: Instead of "_offline", support more granular/accurate
- // built-in exits like:
- // • '_failedCrossOriginRequest'
- // • '_serverDown'
- // • '_clientOffline'
- //
- // ^^In the future, if we want to get really fancy,
- // we could try sending a ping to another CORS-enabled
- // endpoint to see whether it's us or them. Not sure
- // how we'd figure out if it's a failed cross-origin
- // request though...
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- }
- // If the server responded with a specific error...
- else if (xExitResponseHeaderValue){
- responseInfo.exit = xExitResponseHeaderValue;
- }
- // If the server responded with some other misc. error...
- else if (responseInfo.statusCode < 200 || responseInfo.statusCode >= 300) {
- responseInfo.exit = 'error';
- }
- // Otherwise, we'll consider it a success!
- else {
- responseInfo.exit = 'success';
- }
- // Set up `code` as alias for `exit`, for consistency.
- responseInfo.code = responseInfo.exit;
- // Now before proceeding further, check lifecycleInstructions for a match (if there are any configured).
- // > NOTE: We only ever run one of these handlers for any given response!
- var matchingLifecycleInstruction = _.find(requestInfo.lifecycleInstructions, function(lifecycleInstruction) {
- if (lifecycleInstruction.rule === undefined) {
- if (responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
- return false;
- }
- else {
- return true;
- }
- }
- else if (responseInfo.statusCode === lifecycleInstruction.rule) {
- return true;
- }
- else if (responseInfo.exit === lifecycleInstruction.rule) {
- return true;
- }
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: add support for bluebird style dictionary rules
- // (see flaverr.taste at https://npmjs.com/package/flaverr)
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- });//∞
- // If there was a match, then run this intercept/toleration's handler function.
- (function _runInterceptOrTolerationMaybe(proceed){
- if (!matchingLifecycleInstruction) {
- return proceed();
- }//•
- var resultFromHandler;
- if (matchingLifecycleInstruction.handler.constructor.name === 'AsyncFunction') {
- return proceed(new Error('`async` functions are not *yet* fully supported in intercept/tolerate'));
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: add support for this, beginning with something like the
- // following incomplete implementation:
- //
- // ```
- // var interceptPromise;
- // try {
- // interceptPromise = matchingLifecycleInstruction.handler();
- // } catch (err) {
- // if (err === false) { return proceed(undefined, true); }//« special case (`throw false`)
- // else { return proceed(err); }
- // }
- //
- // interceptPromise.then(function(_resultFromHandler){
- // resultFromHandler = _resultFromHandler;
- // proceed(undefined, resultFromHandler);
- // });
- // interceptPromise.catch(function(err) {
- // /* eslint-disable callback-return */
- // if (err === false) { proceed(undefined, true); }//« special case (`throw false`)
- // else { proceed(err); }
- // /* eslint-enable callback-return */
- // });
- // ```
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- }
- else {
- try {
- // FUTURE: do this addition of properties earlier:
- errorInstance.exit = responseInfo.exit;
- errorInstance.code = responseInfo.exit;
- errorInstance.responseInfo = responseInfo;
- resultFromHandler = matchingLifecycleInstruction.handler(errorInstance);
- } catch (err) {
- if (err === false) { return proceed(undefined, true); }//« special case (`throw false`)
- else { return proceed(err); }
- }
- return proceed(undefined, resultFromHandler);
- }
- })(function(err, resultFromInterceptOrTolerate) {
- if (err) {
- throw new Error('The provided custom intercept/tolerate logic threw an unexpected, uncaught error: '+err.stack);
- // FUTURE: better error handling for this case ^^
- }//•
- if (matchingLifecycleInstruction) {
- if (responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
- throw new Error('Unexpected intercept/tolerate logic matched a 2xx/success response, but these methods should only be used for exceptions!');
- // FUTURE: better error handling for this case ^^
- }
- }
- // If a matching `.tolerate()` was encountered, then consider this successful no matter what.
- var tolerateAsIfSuccess = (matchingLifecycleInstruction && matchingLifecycleInstruction.type === 'tolerate');
- // If a matching `.intercept()` was encountered, then consider whatever the intercept handler
- // returned to be our new Error.
- if (matchingLifecycleInstruction && matchingLifecycleInstruction.type === 'intercept') {
- if (!_.isError(resultFromInterceptOrTolerate)) {
- throw new Error('Unexpected value returned from .intercept() handler. Expected an Error instance but instead, got: '+resultFromInterceptOrTolerate);
- // FUTURE: better error handling for this case ^^
- }
- errorInstance = resultFromInterceptOrTolerate;
- }
- // If no custom error callback was specified, but we don't know
- // how to handle an error below, then we'll simply throw a fatal
- // error (this can be caught by `window.onerror` et. al. in order
- // to trigger a fatal error message, e.g. using a devoted user
- // interface component as a global error bus.)
- //
- // This string is used as a prefix for the various error messages
- // that can occur this way throughout the code below.
- var UNHANDLED_ERR_PREFIX_MSG =
- (
- (responseInfo.statusCode===0)?
- 'Unable to send request... are the client and server both online?'
- :'Received unhandled '+responseInfo.statusCode+' error from server.'
- )+' '+
- '(See `responseInfo` property of this error for details). '+
- 'Note that you can negotiate any error using its `exit` or `responseInfo.statusCode` properties.\n'+
- '--\n';
- // ┌─┐┌─┐┌┐┌┌─┐┬─┐┬┌─┐ ┌─┐─┐ ┬┌─┐┌─┐ ┌─┐┌─┐┬ ┬ ┌┐ ┌─┐┌─┐┬┌─ ┌─┐┬ ┬┌┐┌┌─┐┌┬┐┬┌─┐┌┐┌
- // │ ┬├┤ │││├┤ ├┬┘││ ├┤ ┌┴┬┘├┤ │ │ ├─┤│ │ ├┴┐├─┤│ ├┴┐ ├┤ │ │││││ │ ││ ││││
- // └─┘└─┘┘└┘└─┘┴└─┴└─┘ o└─┘┴ └─└─┘└─┘ └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴ └ └─┘┘└┘└─┘ ┴ ┴└─┘┘└┘
- // If a generic callback was provided...
- if (_.isFunction(exitCallbacks)) {
- if (tolerateAsIfSuccess) {
- return exitCallbacks(undefined, resultFromInterceptOrTolerate, responseInfo);
- }
- else if (responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
- return exitCallbacks(undefined, responseInfo.body, responseInfo);
- }
- else {
- errorInstance.stack += '\n'+
- '\n'+
- 'Error Summary:\n'+
- '(see `.responseInfo` for more details)\n'+
- '·-------------·----------------------------------------·\n'+
- '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
- '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
- '| Exit | '+responseInfo.exit+'\n'+
- '| Status Code | '+responseInfo.statusCode+'\n'+
- '·-------------·----------------------------------------·';
- if (responseInfo.body !== undefined) {
- errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
- }
- errorInstance.responseInfo = responseInfo;
- errorInstance.exit = responseInfo.exit;
- errorInstance.code = responseInfo.exit;
- return exitCallbacks(errorInstance, responseInfo.body!==undefined?responseInfo.body:errorInstance, responseInfo);
- }
- }//‡
- // ┌─┐┬ ┬┬┌┬┐┌─┐┬ ┬┌┐ ┌─┐┌─┐┬┌─ ┌┬┐┬┌─┐┌┬┐┬┌─┐┌┐┌┌─┐┬─┐┬ ┬
- // └─┐││││ │ │ ├─┤├┴┐├─┤│ ├┴┐ ││││ │ ││ ││││├─┤├┬┘└┬┘
- // └─┘└┴┘┴ ┴ └─┘┴ ┴└─┘┴ ┴└─┘┴ ┴ ─┴┘┴└─┘ ┴ ┴└─┘┘└┘┴ ┴┴└─ ┴
- // If a dictionary of callbacks was provided...
- else if (_.isObject(exitCallbacks)) {
- // If this isn't the error exit, and a callback exists for it...
- if (responseInfo.exit !== 'error' && exitCallbacks[responseInfo.exit]) {
- // If there's a response body, pass it to the callback.
- if (responseInfo.body !== undefined) {
- return exitCallbacks[responseInfo.exit](tolerateAsIfSuccess ? resultFromInterceptOrTolerate : responseInfo.body, responseInfo);
- }
- // Otherwise, there's no response body.
- // So if this is a "success" response, or any 2xx response,
- // then don't pass a first arg to the callback.
- else if (tolerateAsIfSuccess || responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
- return exitCallbacks['success'](tolerateAsIfSuccess ? resultFromInterceptOrTolerate : undefined, responseInfo);
- }
- // Otherwise, pass an error instance as the first arg of the callback.
- else {
- errorInstance.stack += '\n'+
- '\n'+
- 'Error Summary:\n'+
- '(see `.responseInfo` for more details)\n'+
- '·-------------·----------------------------------------·\n'+
- '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
- '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
- '| Exit | '+responseInfo.exit+'\n'+
- '| Status Code | '+responseInfo.statusCode+'\n'+
- '·-------------·----------------------------------------·';
- if (responseInfo.body !== undefined) {
- errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
- }
- errorInstance.responseInfo = responseInfo;
- errorInstance.exit = responseInfo.exit;
- errorInstance.code = responseInfo.exit;
- return exitCallbacks[responseInfo.exit](errorInstance, responseInfo);
- }
- }
- // Otherwise, if this is a "success" response, or any 2xx response, then...
- else if (tolerateAsIfSuccess || responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
- if (exitCallbacks['success']) {
- // Either forward to the "success" callback (if there is one)
- return exitCallbacks['success'](tolerateAsIfSuccess ? resultFromInterceptOrTolerate : responseInfo.body, responseInfo);
- }
- else {
- // or otherwise do nothing.
- }
- }
- // Otherwise call the error callback.
- else if (exitCallbacks['error']) {
- errorInstance.stack += '\n'+
- '\n'+
- 'Error Summary:\n'+
- '(see `.responseInfo` for more details)\n'+
- '·-------------·----------------------------------------·\n'+
- '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
- '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
- '| Exit | '+responseInfo.exit+'\n'+
- '| Status Code | '+responseInfo.statusCode+'\n'+
- '·-------------·----------------------------------------·';
- if (responseInfo.body !== undefined) {
- errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
- }
- errorInstance.responseInfo = responseInfo;
- errorInstance.exit = responseInfo.exit;
- errorInstance.code = responseInfo.code;
- return exitCallbacks['error'](errorInstance, responseInfo);
- }
- // Or if there isn't an error callback, just throw.
- else {
- errorInstance.stack += '\n'+
- '\n'+
- 'Error Summary:\n'+
- '(see `.responseInfo` for more details)\n'+
- '·-------------·----------------------------------------·\n'+
- '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
- '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
- '| Exit | '+responseInfo.exit+'\n'+
- '| Status Code | '+responseInfo.statusCode+'\n'+
- '·-------------·----------------------------------------·';
- if (responseInfo.body !== undefined) {
- errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
- }
- errorInstance.stack = UNHANDLED_ERR_PREFIX_MSG + errorInstance.stack;
- errorInstance.responseInfo = responseInfo;
- throw errorInstance;
- }
- }//‡
- // ┌┐┌┌─┐ ┌─┐┌─┐┬ ┬ ┌┐ ┌─┐┌─┐┬┌─ ┌─┐┌─┐ ┌─┐┌┐┌┬ ┬ ┬┌─┬┌┐┌┌┬┐
- // ││││ │ │ ├─┤│ │ ├┴┐├─┤│ ├┴┐ │ │├┤ ├─┤│││└┬┘ ├┴┐││││ ││
- // ┘└┘└─┘ └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴ └─┘└ ┴ ┴┘└┘ ┴ ┴ ┴┴┘└┘─┴┘
- // If _no callbacks of any kind_ were provided...
- else if (_.isUndefined(exitCallbacks)) {
- // If this was successful, then do nothing.
- if (tolerateAsIfSuccess || responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
- return;
- }
- // Otherwise, throw.
- else {
- errorInstance.stack += '\n'+
- '\n'+
- 'Error Summary:\n'+
- '(see `.responseInfo` for more details)\n'+
- '·-------------·----------------------------------------·\n'+
- '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
- '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
- '| Exit | '+responseInfo.exit+'\n'+
- '| Status Code | '+responseInfo.statusCode+'\n'+
- '·-------------·----------------------------------------·';
- if (responseInfo.body !== undefined) {
- errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
- }
- errorInstance.stack = UNHANDLED_ERR_PREFIX_MSG + errorInstance.stack;
- errorInstance.responseInfo = responseInfo;
- throw errorInstance;
- }
- }//‡
- else {
- throw new Error('Invalid usage of Cloud.*() method. Provide either a dictionary of callbacks, a single callback function, or NOTHING to `.exec()`.');
- }
- });//_∏_ </cb from † _runInterceptOrTolerationMaybe()>
- });//_∏_ </cb from † _makeAjaxCallWithAppropriateProtocol()>
- // --
- // > Note that we don't return anything at all here.
- // > (That's to ensure userland code doesn't attempt any further chaining or `await`ing.)
- },//</definition of `.exec()` >
- switch: function (){
- deferred.exec.apply(deferred, arguments);
- // --
- // > Note that we don't return anything at all here.
- // > (That's to ensure userland code doesn't attempt any further chaining or `await`ing.)
- },
- // FUTURE: use parley for this instead, if available
- log: function (){
- console.log('Running with `.log()`...');
- this.exec(function(err, result) {
- if (err) {
- console.error();
- console.error('- - - - - - - - - - - - - - - - - - - - - - - -');
- console.error('An error occurred:');
- console.error();
- console.error(err);
- console.error('- - - - - - - - - - - - - - - - - - - - - - - -');
- console.error();
- return;
- }//-•
- console.log();
- if (_.isUndefined(result)) {
- console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
- console.log('Finished successfully.');
- console.log();
- console.log('(There was no result.)');
- console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
- }
- else {
- console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
- console.log('Finished successfully.');
- console.log();
- console.log('Result:');
- console.log();
- console.log(result);
- console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
- }
- console.log();
- });//_∏_
- // --
- // > Note that we don't return anything at all here.
- // > (That's to ensure userland code doesn't attempt any further chaining or `await`ing.)
- }
- };// </define deferred object>
- // ███╗ ███╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ███████╗███████╗ █████╗ ██╗ ██╗██╗ ████████╗███████╗
- // ████╗ ████║██╔════╝██╔══██╗██╔════╝ ██╔════╝ ██╔══██╗██╔════╝██╔════╝██╔══██╗██║ ██║██║ ╚══██╔══╝██╔════╝
- // ██╔████╔██║█████╗ ██████╔╝██║ ███╗█████╗ ██║ ██║█████╗ █████╗ ███████║██║ ██║██║ ██║ ███████╗
- // ██║╚██╔╝██║██╔══╝ ██╔══██╗██║ ██║██╔══╝ ██║ ██║██╔══╝ ██╔══╝ ██╔══██║██║ ██║██║ ██║ ╚════██║
- // ██║ ╚═╝ ██║███████╗██║ ██║╚██████╔╝███████╗ ██████╔╝███████╗██║ ██║ ██║╚██████╔╝███████╗██║ ███████║
- // ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝
- //
- // ███████╗██████╗ ██████╗ ███╗ ███╗ ███████╗███╗ ██╗██████╗ ██████╗ ██████╗ ██╗███╗ ██╗████████╗
- // ██╔════╝██╔══██╗██╔═══██╗████╗ ████║ ██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔═══██╗██║████╗ ██║╚══██╔══╝
- // █████╗ ██████╔╝██║ ██║██╔████╔██║ █████╗ ██╔██╗ ██║██║ ██║██████╔╝██║ ██║██║██╔██╗ ██║ ██║
- // ██╔══╝ ██╔══██╗██║ ██║██║╚██╔╝██║ ██╔══╝ ██║╚██╗██║██║ ██║██╔═══╝ ██║ ██║██║██║╚██╗██║ ██║
- // ██║ ██║ ██║╚██████╔╝██║ ╚═╝ ██║ ███████╗██║ ╚████║██████╔╝██║ ╚██████╔╝██║██║ ╚████║ ██║
- // ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝
- //
- // ██████╗ ███████╗███████╗██╗███╗ ██╗██╗████████╗██╗ ██████╗ ███╗ ██╗
- // ██╔══██╗██╔════╝██╔════╝██║████╗ ██║██║╚══██╔══╝██║██╔═══██╗████╗ ██║
- // ██║ ██║█████╗ █████╗ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║
- // ██║ ██║██╔══╝ ██╔══╝ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║
- // ██████╔╝███████╗██║ ██║██║ ╚████║██║ ██║ ██║╚██████╔╝██║ ╚████║
- // ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
- //
- // Now set up the endpoint definition.
- //////////////////////////////////////////////////////////////////////
- // If a function was supplied, call it and use the dictionary
- // it returns as our request info.
- //////////////////////////////////////////////////////////////////////
- // e.g. function (argins){
- // return {
- // verb: 'post',
- // url: '/foo/bar',
- // params: {
- // whatever: 'you want' + ' in here maybe using '+argins.whatever
- // },
- // headers: {}
- // }
- // }
- if (typeof appLevelSdkEndpointDef === 'function') {
- var returnedFromEndpointDefFn = appLevelSdkEndpointDef.apply(this, argins);
- if (typeof returnedFromEndpointDefFn !== 'object') {
- throw new Error('Consistency violation: Function for CloudSDK endpoint (`'+methodName+'`) returned an invalid result. The return value of the specified function is not a dictionary! If a function is supplied for an endpoint definition, it must return a dictionary containing a `verb` and a `url`. The returned dictionary may also contain dynamic, per-request header & parameter values.');
- }
- if (!_.isUndefined(returnedFromEndpointDefFn.headers)) {
- deferred = deferred.headers(returnedFromEndpointDefFn.headers);
- }
- else if (options.headers) {
- deferred = deferred.headers(options.headers);
- }
- if (!_.isUndefined(returnedFromEndpointDefFn.protocol)) {
- deferred = deferred.protocol(returnedFromEndpointDefFn.protocol);
- }
- else if (options.protocol) {
- deferred.protocol(options.protocol);
- }
- else { deferred.protocol(DEFAULT_PROTOCOL_NAME); }
- requestInfo.verb = returnedFromEndpointDefFn.verb;
- requestInfo.url = returnedFromEndpointDefFn.url;
- requestInfo.params = argins;
- }
- // If a dictionary was supplied, use that as our request info.
- //////////////////////////////////////////////////////////////////////
- // e.g. {
- // verb: 'post',
- // url: '/foo/bar',
- // protocol: 'io.socket',//optional, defaults to 'jQuery'
- // headers: {'x-auth': 'foo'},//optional, defaults to undefined
- // }
- else if (appLevelSdkEndpointDef && typeof appLevelSdkEndpointDef === 'object') {
- if (!_.isUndefined(appLevelSdkEndpointDef.headers)) {
- deferred.headers(appLevelSdkEndpointDef.headers);
- }
- else if (options.headers) {
- deferred = deferred.headers(options.headers);
- }
- if (!_.isUndefined(appLevelSdkEndpointDef.protocol)) {
- deferred.protocol(appLevelSdkEndpointDef.protocol);
- }
- else if (options.protocol) {
- deferred.protocol(options.protocol);
- }
- else { deferred.protocol(DEFAULT_PROTOCOL_NAME); }
- requestInfo.verb = appLevelSdkEndpointDef.verb;
- requestInfo.url = appLevelSdkEndpointDef.url;
- requestInfo.params = argins;
- }
- // If a string was supplied, expand and use that as our request info.
- //////////////////////////////////////////////////////////////////////
- // e.g. "POST /api/v1/lawnmowers/foo/inputs/bar"
- else if (typeof appLevelSdkEndpointDef === 'string') {
- if (options.headers) {
- deferred = deferred.headers(options.headers);
- }
- // Set up default protocol.
- if (options.protocol) {
- deferred.protocol(options.protocol);
- }
- else { deferred.protocol(DEFAULT_PROTOCOL_NAME); }
- // And then fold in the other pieces of request info.
- requestInfo.verb = appLevelSdkEndpointDef.replace(/^\s*([^\/\s]+)\s*\/.*$/, '$1');
- requestInfo.url = appLevelSdkEndpointDef.replace(/^\s*[^\/\s]+\s*\/(.*)$/, '/$1');
- requestInfo.params = argins;
- }
- else {
- throw new Error('Consistency violation: Something happened to CloudSDK endpoint (`'+methodName+'`). This was not noticed initially when building up CloudSDK endpoints, but this endpoint is now 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.');
- }
- // Now template in URL pattern vars from the runtime request args.
- /////////////////////////////////////////////////////////////////////////////
- // Find keys in `params` which are route parameters
- // (e.g. referenced by the endpoint URL)
- // > Note that we're not actually interested in the return value
- // > from this first `.replace()` here.
- var routeParameters = {};
- requestInfo.url.replace(/(\:[^\/\:\.\?]+\??)/g, function ($all, $1){
- var routeParamName = $1.replace(/^\:/, '').replace(/\??$/, '');
- // Optional:
- if ($1.match(/\?$/)) {
- if (requestInfo.params && requestInfo.params[routeParamName]) {
- routeParameters[routeParamName] = requestInfo.params[routeParamName];
- }
- }
- // Mandatory:
- else {
- if (!requestInfo.params || requestInfo.params[routeParamName] === undefined) {
- throw new Error('Missing required param: `'+routeParamName+'`');
- }
- routeParameters[routeParamName] = requestInfo.params[routeParamName];
- }
- });//∞
- // Then delete them from the `params` object
- Object.keys(routeParameters).forEach(function (paramName){
- delete requestInfo.params[paramName];
- });
- // Now stick the route parameters into the destination url
- requestInfo.url = requestInfo.url.replace(/(\:[^\/\:\.\?]+\??)/g, function ($all, $1){
- var routeParamName = $1.replace(/^\:/, '').replace(/\??$/, '');
- if (routeParameters[routeParamName] === undefined) { return ''; }
- return routeParameters[routeParamName];
- });
- // Prepend the API base URL to `requestInfo.url`.
- /////////////////////////////////////////////////////////////////////////////
- requestInfo.url = options.apiBaseUrl + requestInfo.url;
- // Ensure verb exists, and then lower-case it.
- /////////////////////////////////////////////////////////////////////////////
- if (!requestInfo.verb) { throw new Error('CloudSDK endpoint (`'+methodName+'`) is invalid: No HTTP verb specified. Please specify an HTTP verb (e.g. `GET`, `POST`, etc.)'); }
- requestInfo.verb = (requestInfo.verb || 'get').toLowerCase();
- // Attach the `requestInfo` as a property on the Deferred object itself, for easier integration
- // with 3rd-party tools (e.g. autocomplete)
- deferred.requestInfo = requestInfo;
- // Return the deferred object.
- return deferred;
- };//ƒ </ _helpCallCloudMethod >
- // Primary definition of this Cloud.* method()
- memo[methodName] = function () {
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: If no `args` configured, then check route params (url pattern
- // variables). If there are any, attempt to use their names... maybe?
- // Could actually be MORE confusing though-- needs to be played with.
- //
- // UPDATE: OK probably best not to do this actually.
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- if (!appLevelSdkEndpointDef.args && arguments.length > 0) {
- throw new Error(
- 'Cannot call this Cloud.*() method with serial usage because Cloud SDK is not aware of the appropriate parameter names! Please pass in named parameter values using .with({…}) instead--or if you\'re the implementor of the corresponding Sails action, change it on the backend and regenerate the SDK so that this method is configured with an `args` array.\n'+
- ' [?] If you\'re unsure, visit https://sailsjs.com/support for help.'
- );
- }
- // Parse arguments into argins
- var argins = _.reduce(arguments, function(argins, argin, i){
- if (!(appLevelSdkEndpointDef.args[i])) {
- throw new Error('Invalid usage with serial arguments: Received unexpected '+(i===0?'first':i===1?'second':i===2?'third':(i+1)+'th')+' argument.');
- }
- // Reject special notation.
- // > Remember, if we made it to this point, we know it's valid b/c it's already been checked.
- if (appLevelSdkEndpointDef.args[i] === '{*}') {
- if (argin !== undefined && (!_.isObject(argin) || _.isArray(argin) || _.isFunction(argin))) {
- throw new Error('Invalid usage with serial arguments: If provided, expected '+(i===0?'first':i===1?'second':i===2?'third':(i+1)+'th')+' argument to be a dictionary (plain JavaScript object, like `{}`). But instead, got: '+argin+'');
- } else if (argin !== undefined && _.intersection(_.keys(argins), _.keys(argin)).length > 0) {
- throw new Error('Invalid usage with serial arguments: If provided, expected '+(i===0?'first':i===1?'second':i===2?'third':(i+1)+'th')+' argument to have keys which DO NOT overlap with other already-configured argins! But in reality, it contained conflicting keys: '+_.intersection(_.keys(argins), _.keys(argin))+'');
- }
- _.extend(argins, argin);
- } else {
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // Note: For design considerations & historical context, see:
- // • https://github.com/node-machine/machine/commit/fa3829fa637a267793be4a7fb573e008581c4656
- // • https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facec
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: Support declaring variadic usage
- // https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facecR58
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // FUTURE: Support declaring spread arguments
- // https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facecR66
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- // Otherwise interpret this as the code name of an input
- argins[appLevelSdkEndpointDef.args[i]] = argin;
- }
- return argins;
- }, {});//= (∞)
- return _helpCallCloudMethod(argins);
- };//ƒ
- // Escape hatch that always allows using named parameters.
- memo[methodName].with = function (argins) {
- return _helpCallCloudMethod(argins);
- };//ƒ
- return memo;
- }, {});//</ _.reduce() :: each defined endpoint method >
- // Remove the `.setup()` method, now that it's been called.
- delete Cloud.setup;
- // Now attach the methods
- _.extend(Cloud, methods);
- };
- return Cloud;
- });
|