cloud.js 90 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745
  1. /**
  2. * cloud.js
  3. * (high-level AJAX library)
  4. *
  5. * > This is now part of parasails. Originally branched from the original
  6. * > Cloud SDK library at v1.0.1. (All future development of Cloud SDK will be
  7. * > as part of parasails.)
  8. *
  9. * Copyright (c) 2015-2017, Mike McNeil, Scott Gress, Sails Co. (https://sailsjs.com/about)
  10. * Copyright (c) 2014, Mike McNeil, Balderdash Design Co. (http://balderdash.co)
  11. * MIT License
  12. * ---------------------------------------------------------------------------------------------
  13. * ## Basic Usage
  14. *
  15. * Step 1:
  16. *
  17. * ```
  18. * Cloud.setup({ doSomething: 'POST /api/v1/somethings/:id/do' });
  19. * ```
  20. * ^^Note that this can also be compiled automatically from your Sails app's routes using a script.
  21. *
  22. * Step 2:
  23. *
  24. * ```
  25. * var result = await Cloud.doSomething(8);
  26. * ```
  27. *
  28. * Or:
  29. * ```
  30. * var result = await Cloud.doSomething.with({id: 8, foo: ['bar', 'baz']});
  31. * ```
  32. * ---------------------------------------------------------------------------------------------
  33. */
  34. (function(global, factory){
  35. var _;
  36. var io;
  37. var $;
  38. var SAILS_LOCALS;
  39. var location;
  40. var File;
  41. var FormData;
  42. // First, handle optional deps that are gleaned from the global state:
  43. // > Note: Instead of throwing, we ignore invalid globals.
  44. // > (Remember the bug w/ the File global that happened in Socket.io
  45. // > back in ~2015!)
  46. // =====================================================================
  47. if (global.location !== undefined) {
  48. if (global.location && typeof global.location === 'object' && (global.location.constructor.name === 'Location' || global.location.constructor.toString() === '[object Location]')) {
  49. location = global.location;
  50. }
  51. }//fi
  52. if (global.File !== undefined) {
  53. if (global.File && typeof global.File === 'function' && global.File.name === 'File') {
  54. File = global.File;
  55. }
  56. }//fi
  57. if (global.FormData !== undefined) {
  58. if (global.FormData && typeof global.FormData === 'function' && global.FormData.name === 'FormData') {
  59. FormData = global.FormData;
  60. }
  61. }//fi
  62. // Then, load the rest of the deps:
  63. // =====================================================================
  64. //˙°˚°·.
  65. //‡CJS ˚°˚°·˛
  66. if (typeof exports === 'object' && typeof module !== 'undefined') {
  67. var _require = require;// eslint-disable-line no-undef
  68. var _module = module;// eslint-disable-line no-undef
  69. // required deps:
  70. if (typeof _ === 'undefined') {
  71. try {
  72. _ = _require('@sailshq/lodash');
  73. } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
  74. }//fi
  75. if (typeof _ === 'undefined') {
  76. try {
  77. _ = _require('lodash');
  78. } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
  79. }//fi
  80. // optional deps:
  81. try { $ = _require('jquery'); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
  82. try {
  83. io = _require('socket.io-client');
  84. var sailsIO = _require('sails.io.js');
  85. // Instantiate the library (and start auto-connecting)
  86. io = sailsIO(io);
  87. // Disable logging
  88. io.sails.environment = 'production';
  89. // Note that, if there is no location global, then after one tick,
  90. // if `io.sails.url` has still not been set, weird errors will emerge.
  91. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  92. // FUTURE: figure out a way to provide a better err msg about this--
  93. // i.e. specifically the case where `.setup()` isn't called within one tick.
  94. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  95. } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } }
  96. SAILS_LOCALS = undefined;
  97. // export:
  98. _module.exports = factory(_, io, $, SAILS_LOCALS, location, File, FormData);
  99. }
  100. //˙°˚°·
  101. //‡AMD ˚¸
  102. else if(typeof define === 'function' && define.amd) {// eslint-disable-line no-undef
  103. 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`.');
  104. // var _define = define;// eslint-disable-line no-undef
  105. // _define(['_', 'sails.io.js', '$', 'SAILS_LOCALS', 'location', 'file'], factory);
  106. }
  107. //˙°˚˙°·
  108. //‡NUDE ˚°·˛
  109. else {
  110. // required deps:
  111. 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`.)'); }
  112. _ = global._;
  113. // optional deps:
  114. if (global.io !== undefined) {
  115. if (typeof global.io !== 'function') {
  116. 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`.)');
  117. }
  118. else if (typeof global.io.socket === 'undefined') {
  119. 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!');
  120. }
  121. else {
  122. io = global.io;
  123. }
  124. }//fi
  125. if (global.$ !== undefined) {
  126. if (typeof global.$ !== 'function') {
  127. 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`.)');
  128. }
  129. else {
  130. $ = global.$;
  131. }
  132. }//fi
  133. if (global.SAILS_LOCALS !== undefined) {
  134. if (!_.isObject(global.SAILS_LOCALS)) {
  135. 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.)');
  136. }
  137. else {
  138. SAILS_LOCALS = global.SAILS_LOCALS;
  139. }
  140. }//fi
  141. // export:
  142. if (global.Cloud) { throw new Error('Cannot expose global variable: Conflicting global (`cloud`) already exists!'); }
  143. global.Cloud = factory(_, io, $, SAILS_LOCALS, location, File, FormData);
  144. }
  145. })(this, function (_, io, $, SAILS_LOCALS, location, File, FormData){
  146. /**
  147. * @param {String} negotiationRule
  148. *
  149. * @throws {Error} If rule is invalid or absent
  150. */
  151. function _verifyErrorNegotiationRule(negotiationRule) {
  152. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  153. // FUTURE: add support for parley/flaverr/bluebird/lodash-style dictionary negotiation rules
  154. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  155. if (_.isNumber(negotiationRule) && Math.floor(negotiationRule) === negotiationRule) {
  156. if (negotiationRule > 599 || negotiationRule < 0) {
  157. throw new Error('Invalid error negotiation rule: `'+negotiationRule+'`. If a status code is provided, it must be between zero and 599.');
  158. }
  159. }
  160. else if (_.isString(negotiationRule) && negotiationRule) {
  161. // Ok, we'll assume it's fine
  162. }
  163. else {
  164. var suffix = '';
  165. if (negotiationRule === undefined || _.isFunction(negotiationRule)) {
  166. 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.';
  167. }
  168. 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);
  169. }
  170. }
  171. /**
  172. * Cloud (SDK)
  173. *
  174. * After setup, this dictionary will have a method for each declared endpoint.
  175. * Each key will be a function which sends an HTTP or socket request to a
  176. * particular endpoint.
  177. *
  178. * ### Setup
  179. *
  180. * ```
  181. * Cloud.setup({
  182. * apiBaseUrl: 'https://example.com',
  183. * usageOpts: {
  184. * arginStyle: 'serial'
  185. * },
  186. * methods: {
  187. * doSomething: 'PUT /api/v1/projects/:id',
  188. * // ...
  189. * }
  190. * });
  191. * ```
  192. *
  193. * > Note that you should avoid having an endpoint method named "setup", for obvious reasons.
  194. * > (Technically, it should work anyway though. But yeah, no reason to tempt the fates.)
  195. *
  196. * ### Basic Usage
  197. *
  198. * ```
  199. * var user = await Cloud.findOneUser(3);
  200. * ```
  201. *
  202. * ```
  203. * var user = await Cloud.findOneUser.with({ id: 3 });
  204. * ```
  205. *
  206. * ```
  207. * Cloud.doSomething.with({
  208. * someParam: ['things', 3235, null, true, false, {}, []]
  209. * someOtherParam: 2523,
  210. * etc: 'more things'
  211. * }).exec(function (err, responseBody, responseObjLikeJqXHR) {
  212. * if (err) {
  213. * // ...
  214. * return;
  215. * }
  216. *
  217. * // ...
  218. * });
  219. * ```
  220. *
  221. * ### Negotiating Errors
  222. * ```
  223. * Cloud.signup.with({...})
  224. * .switch({
  225. * error: function (err) { ... },
  226. * usernameAlreadyInUse: function (recommendedAlternativeUsernames) { ... },
  227. * emailAddressAlreadyInUse: function () { ... },
  228. * success: function () { ... }
  229. * });
  230. * ```
  231. *
  232. * ### Using WebSockets
  233. * ```
  234. * Cloud.doSomething.with({...})
  235. * .protocol('jQuery')
  236. * .exec(...);
  237. * ```
  238. *
  239. * ```
  240. * Cloud.doSomething.with({...})
  241. * .protocol('io.socket')
  242. * .exec(...);
  243. * ```
  244. *
  245. * ##### Providing a particular jQuery or SailsSocket instance
  246. *
  247. * ```
  248. * Cloud.doSomething.with({...})
  249. * .protocol(io.socket)
  250. * .exec(...);
  251. * ```
  252. *
  253. * ```
  254. * Cloud.doSomething.with({...})
  255. * .protocol($)
  256. * .exec(...);
  257. * ```
  258. *
  259. * ### Using Custom Headers
  260. * ```
  261. * Cloud.doSomething.with({...})
  262. * .headers({
  263. * 'X-Auth': 'whatever'
  264. * })
  265. * .exec(...);
  266. * ```
  267. *
  268. * ### CSRF Protection
  269. *
  270. * It `SAILS_LOCALS._csrf` is defined, then it will be sent
  271. * as the "x-csrf-token" header for all Cloud.* requests, automatically.
  272. *
  273. */
  274. var Cloud = {};
  275. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  276. // FUTURE: finish this when time allows (would be better to have it work by attaching dedicated
  277. // nav methods rather than a generic nav method though)
  278. // ```
  279. // // A mapping of names of view actions to URL
  280. // // > provided to `.setup()`, for use in .navigate()
  281. // var _navigableUrlsByViewActionName;
  282. // /**
  283. // * Cloud.navigate()
  284. // *
  285. // * Call this function to navigate to a different web page.
  286. // * (Be sure and call it *before* trying to use any of the endpoint methods!)
  287. // *
  288. // * @param {String} destination
  289. // * A URL or the name of a view action.
  290. // */
  291. // Cloud.navigate = function(destination) {
  292. // var doesBeginWithSlash = _.isString(destination) && destination.match(/^\//);
  293. // var doesBeginWithHttp = _.isString(destination) && destination.match(/^http/);
  294. // var isProbablyTheNameOfAViewAction = _.isString(destination) && destination.match(/^view/);
  295. // if (!_.isString(destination) || !(doesBeginWithSlash || doesBeginWithHttp || isProbablyTheNameOfAViewAction)) {
  296. // throw new Error('Bad usage: Cloud.navigate() should be called with a URL or the name of a view action.');
  297. // }
  298. // if (!_navigableUrlsByViewActionName) {
  299. // 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)');
  300. // }
  301. // };
  302. // ```
  303. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  304. /**
  305. * Cloud.setup()
  306. *
  307. * Call this function once, when the page loads.
  308. * (Be sure and call it *before* trying to use any of the endpoint methods!)
  309. *
  310. * @param {Dictionary} options
  311. * @required {Dictionary} methods
  312. * @optional {Dictionary} links
  313. * @optional {Dictionary} apiBaseUrl
  314. */
  315. Cloud.setup = function(options) {
  316. options = options || {};
  317. if (!_.isObject(options.methods) || _.isArray(options.methods) || _.isFunction(options.methods)) {
  318. throw new Error('Cannot .setup() Cloud SDK: `methods` must be provided as a dictionary of addresses and definitions.');
  319. }//•
  320. // Determine the proper API base URL
  321. if (!options.apiBaseUrl) {
  322. if (location) {
  323. options.apiBaseUrl = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: '');
  324. }
  325. else {
  326. 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").');
  327. }
  328. }//fi
  329. // Apply the base URL for the benefit of WebSockets (if relevant):
  330. if (io) {
  331. io.sails.url = options.apiBaseUrl;
  332. }//fi
  333. // The name of the default protocol.
  334. var DEFAULT_PROTOCOL_NAME = 'jQuery';
  335. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  336. // FUTURE: finish this when time allows (would be better to have it work by attaching dedicated
  337. // nav methods rather than a generic nav method though)
  338. // ```
  339. // // Save a reference to the mapping of navigable URLs by view action name (if provided).
  340. // _navigableUrlsByViewActionName = options.links || {};
  341. // ```
  342. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  343. // Interpret methods
  344. var methods = _.reduce(options.methods, function(memo, appLevelSdkEndpointDef, methodName) {
  345. if (methodName === 'setup') {
  346. 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...)');
  347. }
  348. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  349. // FUTURE: finish this when time allows (would be better to have it work by attaching dedicated
  350. // nav methods rather than a generic nav method though)
  351. // ```
  352. // if (methodName === 'navigate') {
  353. // 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...)');
  354. // }
  355. // ```
  356. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  357. // Validate the endpoint definition.
  358. ////////////////////////////////////////////////////////////////////////////////////////////////
  359. var _verbToCheck;
  360. var _urlToCheck;
  361. if (typeof appLevelSdkEndpointDef === 'function') {
  362. // We can't really check functions, so we just let it through.
  363. }
  364. else {
  365. if (appLevelSdkEndpointDef && typeof appLevelSdkEndpointDef === 'object') {
  366. // Must have `verb` and `url` properties.
  367. _verbToCheck = appLevelSdkEndpointDef.verb;
  368. _urlToCheck = appLevelSdkEndpointDef.url;
  369. }
  370. else if (typeof appLevelSdkEndpointDef === 'string') {
  371. // Must be able to parse `verb` and `url`.
  372. _verbToCheck = appLevelSdkEndpointDef.replace(/^\s*([^\/\s]+)\s*\/.*$/, '$1');
  373. _urlToCheck = appLevelSdkEndpointDef.replace(/^\s*[^\/\s]+\s*\/(.*)$/, '/$1');
  374. }
  375. else {
  376. 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.');
  377. }
  378. // --•
  379. // `verb` must be valid.
  380. if (typeof _verbToCheck !== 'string' || _verbToCheck === '') {
  381. throw new Error('CloudSDK endpoint (`'+methodName+'`) is invalid: An endpoint\'s `verb` should be defined as a non-empty string.');
  382. }
  383. // `url` must be valid.
  384. if (typeof _urlToCheck !== 'string' || _urlToCheck === '') {
  385. throw new Error('CloudSDK endpoint (`'+methodName+'`) is invalid: An endpoint\'s `url` should be defined as a non-empty string.');
  386. }
  387. }
  388. // Build the actual method that will be called at runtime:
  389. ////////////////////////////////////////////////////////////////////////
  390. var _helpCallCloudMethod = function (argins) {
  391. //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
  392. // There are 3 ways to define an SDK wrapper for a cloud endpoint.
  393. //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
  394. var requestInfo = {
  395. // • the HTTP verb (aka HTTP "method" -- we're just using "verb" for clarity)
  396. verb: undefined,
  397. // • the path part of the URL
  398. url: undefined,
  399. // • a dictionary of request data
  400. // (depending on the circumstances, these params will be encoded directly
  401. // into either the url path, the querystring, or the request body)
  402. params: undefined,
  403. // • a dictionary of custom request headers
  404. headers: undefined,
  405. // • the protocol name (e.g. "jQuery" or "io.socket")
  406. protocolName: undefined,
  407. // • the protocol instance (e.g. actual reference to `$` or `io.socket`)
  408. protocolInstance: undefined,
  409. // • an array of conditional lifecycle instructions from userland .intercept() / .tolerate() calls, if any are configured
  410. lifecycleInstructions: [],
  411. };
  412. // ██████╗ ██╗ ██╗██╗██╗ ██████╗ ██████╗ ███████╗███████╗███████╗██████╗ ██████╗ ███████╗██████╗
  413. // ██╔══██╗██║ ██║██║██║ ██╔══██╗ ██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██╔══██╗
  414. // ██████╔╝██║ ██║██║██║ ██║ ██║ ██║ ██║█████╗ █████╗ █████╗ ██████╔╝██████╔╝█████╗ ██║ ██║
  415. // ██╔══██╗██║ ██║██║██║ ██║ ██║ ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██╔══██╗██╔══██╗██╔══╝ ██║ ██║
  416. // ██████╔╝╚██████╔╝██║███████╗██████╔╝ ██████╔╝███████╗██║ ███████╗██║ ██║██║ ██║███████╗██████╔╝
  417. // ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═════╝
  418. //
  419. // ██████╗ ██████╗ ██╗███████╗ ██████╗████████╗
  420. // ██╔═══██╗██╔══██╗ ██║██╔════╝██╔════╝╚══██╔══╝
  421. // ██║ ██║██████╔╝ ██║█████╗ ██║ ██║
  422. // ██║ ██║██╔══██╗██ ██║██╔══╝ ██║ ██║
  423. // ╚██████╔╝██████╔╝╚█████╔╝███████╗╚██████╗ ██║
  424. // ╚═════╝ ╚═════╝ ╚════╝ ╚══════╝ ╚═════╝ ╚═╝
  425. //
  426. // Used for avoiding accidentally creating multiple promises when
  427. // using .then() or .catch().
  428. var _promise;
  429. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  430. // FUTURE: add support for omens so we get better stack traces, particularly
  431. // when running this in a Node.js environment.
  432. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  433. // Return a dictionary of functions (to allow for "deferred object" usage.)
  434. var deferred = {
  435. // Allow request headers to be configured.
  436. /////////////////////////////////////////////////////////////////////////////
  437. headers: function (_customRequestHeaders){
  438. if (!_.isObject(_customRequestHeaders)) {
  439. throw new Error('Invalid request headers: Must be specified as a dictionary, where each key has a string value.');
  440. }
  441. requestInfo.headers = _.extend(requestInfo.headers||{}, _customRequestHeaders);
  442. return deferred;
  443. },
  444. // Allow the protocol to be configured on a per-request basis.
  445. /////////////////////////////////////////////////////////////////////////////
  446. protocol: function (_protocolNameOrInstance){
  447. if (typeof _protocolNameOrInstance === 'string') {
  448. switch (_protocolNameOrInstance) {
  449. case 'jQuery':
  450. requestInfo.protocolName = 'jQuery';
  451. if ($ === undefined) {
  452. throw new Error('Could not access jQuery: `$` is undefined.');
  453. }
  454. else {
  455. requestInfo.protocolInstance = $;
  456. }
  457. break;
  458. case 'io.socket':
  459. requestInfo.protocolName = 'io.socket';
  460. if (typeof io === 'undefined') {
  461. throw new Error('Could not access `io.socket`: `io` is undefined.');
  462. }
  463. else if (typeof io !== 'function') {
  464. throw new Error('Could not access `io.socket`: `io` is invalid:' + io);
  465. }
  466. else if (typeof io.socket === 'undefined') {
  467. 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!');
  468. }
  469. else {
  470. requestInfo.protocolInstance = io.socket;
  471. }
  472. break;
  473. default:
  474. throw new Error('Unrecognized protocol: `'+_protocolNameOrInstance+'`. Use "jQuery" or "io.socket".');
  475. }
  476. }
  477. else if (_.isObject(_protocolNameOrInstance) || _.isFunction(_protocolNameOrInstance)) {
  478. if (_protocolNameOrInstance.name === 'jQuery') {
  479. requestInfo.protocolName = 'jQuery';
  480. requestInfo.protocolInstance = _protocolNameOrInstance;
  481. }
  482. else if (_protocolNameOrInstance.constructor.name === 'SailsSocket') {
  483. requestInfo.protocolName = 'io.socket';
  484. requestInfo.protocolInstance = _protocolNameOrInstance;
  485. }
  486. else if (_protocolNameOrInstance.toString() === '[Package: machinepack-http]') {
  487. requestInfo.protocolName = 'machinepack-http';
  488. requestInfo.protocolInstance = _protocolNameOrInstance;
  489. }
  490. // FUTURE: maybe "axios"?
  491. // FUTURE: maybe "fetch"?
  492. // FUTURE: maybe "request"?
  493. else {
  494. throw new Error('Unrecognized instance provided to `.protocol()`: `'+_protocolNameOrInstance+'`');
  495. }
  496. }
  497. else {
  498. throw new Error('Unrecognized protocol: `'+_protocolNameOrInstance+'`. Use "jQuery" or "io.socket".');
  499. }
  500. return deferred;
  501. },//</ implementation of `.protocol()`>
  502. // Allow intercepting the response before resolution/rejection occurs.
  503. // (This is basically an "after receiving response" lifecycle callback.)
  504. /////////////////////////////////////////////////////////////////////////////
  505. intercept: function (negotiationRule, handler) {
  506. _verifyErrorNegotiationRule(negotiationRule);
  507. if (!_.isFunction(handler)) {
  508. throw new Error('Invalid 2nd argument to `.intercept()`. Expecting a handler function.');
  509. }
  510. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  511. // FUTURE: Add a best-effort check to make sure there is no pre-existing rule
  512. // that matches this one (i.e. already previously registered using .tolerate()
  513. // or .intercept())
  514. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  515. requestInfo.lifecycleInstructions.push({
  516. type: 'intercept',
  517. rule: negotiationRule,
  518. handler: handler
  519. });
  520. return deferred;
  521. },
  522. // Allow explicitly tolerating certain kinds of responses before resolution/rejection occurs.
  523. // (This causes control flow convergence by using `.intercept()` + throwing a special value)
  524. /////////////////////////////////////////////////////////////////////////////
  525. tolerate: function (_negotiationRuleMaybe, _handlerMaybe) {
  526. var handler;
  527. var negotiationRule;
  528. if (_handlerMaybe === undefined && _.isFunction(_negotiationRuleMaybe)) {
  529. handler = _negotiationRuleMaybe;
  530. }
  531. else {
  532. negotiationRule = _negotiationRuleMaybe;
  533. handler = _handlerMaybe;
  534. }
  535. if (negotiationRule !== undefined) {
  536. _verifyErrorNegotiationRule(negotiationRule);
  537. }
  538. if (handler !== undefined && !_.isFunction(handler)) {
  539. throw new Error('Invalid 2nd argument. to `.tolerate()`. Expecting a handler function.');
  540. }
  541. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  542. // FUTURE: Add a best-effort check to make sure there is no pre-existing rule
  543. // that matches this one (i.e. already previously registered using .tolerate()
  544. // or .intercept())
  545. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  546. requestInfo.lifecycleInstructions.push({
  547. type: 'tolerate',
  548. rule: negotiationRule,
  549. handler: handler?
  550. handler
  551. :
  552. function(){ return; }
  553. });
  554. return deferred;
  555. },
  556. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  557. // Looking for the EarlyReturnSignal stuff?
  558. //
  559. // See https://stackoverflow.com/a/43402123/486547 and specifically also
  560. // https://stackoverflow.com/questions/29499582/how-to-properly-break-out-of-a-promise-chain#comment80446341_43402123
  561. // (there may be a way to do this more elegantly without requiring the calling
  562. // code environment to be aware of our special Errors-- but it's not worth it
  563. // as-is. Too much black magic!)
  564. //
  565. // > More notes & background leading up to this:
  566. // > https://gist.github.com/mikermcneil/c1bc2d57f5bedae810295e5ed8c5f935
  567. // >
  568. // > (Also check out the commit history of the original `caviar` repo.)
  569. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  570. then: function (){
  571. // console.log('in implementation of `then()`...');
  572. var promise = deferred.toPromise();
  573. // console.log('obj:',promise);
  574. return promise.then.apply(promise, arguments);
  575. },
  576. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  577. // FUTURE: use parley for all this instead, if we can find a way to keep it
  578. // from being too enormous when browserified
  579. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  580. toPromise: function (){
  581. if (typeof Promise === 'undefined') { throw new Error('Cannot use this approach: `Promise` constructor not available in current environment.'); }
  582. if (_promise) {
  583. // console.log('using catched promise...');
  584. return _promise;
  585. }
  586. // console.log('instantiating new promise!');
  587. _promise = new Promise(function(resolve, reject){// eslint-disable-line no-undef
  588. try {
  589. deferred.exec(function(err, resultMaybe) {
  590. if (err){
  591. // console.log('calling reject..');
  592. return reject(err);
  593. }
  594. // console.log('calling resolve..');
  595. return resolve(resultMaybe);
  596. });//_∏_
  597. } catch (err) {
  598. // console.log('EXEC THREW ERROR!',err);
  599. // console.log('CALLED REJECT IN NATIVE CATCH BLOCK!');
  600. reject(err);
  601. }
  602. });//_∏_
  603. return _promise;
  604. },
  605. // Allow the AJAX request to actually be sent.
  606. /////////////////////////////////////////////////////////////////////////////
  607. exec: function (exitCallbacks){
  608. if (exitCallbacks) {
  609. if (!_.isObject(exitCallbacks) && !_.isFunction(exitCallbacks)) {
  610. 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.');
  611. }
  612. else if (_.isObject(exitCallbacks) && exitCallbacks.success && !_.isFunction(exitCallbacks.success)) {
  613. throw new Error('If specified, `success` callback must be a function.');
  614. }
  615. else if (_.isObject(exitCallbacks) && exitCallbacks.error && !_.isFunction(exitCallbacks.error)) {
  616. throw new Error('If specified, `error` callback must be a function.');
  617. }
  618. }
  619. // Just in case, build an error instance beforehand.
  620. // (This ensures it has a good stack trace.)
  621. var errorInstance = new Error('Endpoint (`'+methodName+'`) responded with an error (or the request failed).');
  622. // Give the error a special `name` property to ease negotiation
  623. // (vs. other unrelated things like typos in argins)
  624. errorInstance.name = 'CloudError';
  625. // If present, use CSRF token from `SAILS_LOCALS` as the `x-csrf-token`
  626. // request header for all non-GET requests.
  627. // (Unless of course there's another x-csrf-token header already specified.)
  628. if (_.isObject(SAILS_LOCALS) && typeof SAILS_LOCALS._csrf !== 'undefined') {
  629. if (_.isUndefined(requestInfo.headers)) {
  630. requestInfo.headers = {};
  631. }// >-
  632. if (!requestInfo.headers['x-csrf-token']) {
  633. requestInfo.headers['x-csrf-token'] = SAILS_LOCALS._csrf;
  634. }
  635. }//fi
  636. // Finally, use the appropriate protocol to actually send the request and
  637. // send back the response to the code that called this `Cloud.*()` method.
  638. (function _makeAjaxCallWithAppropriateProtocol(proceed){
  639. // First, tease apart text params and file params.
  640. var textParamsByFieldName = requestInfo.params;
  641. // Check for file uploads.
  642. //
  643. // If `File`+`FormData` constructors are available, check to
  644. // see if any of the param values are File instances. If
  645. // they are, then remove them from a shallow clone of the
  646. // params dictionary, and set them up separately.
  647. // (The files will be attached to the request _after_
  648. // the text parameters.)
  649. var filesByFieldName = {};
  650. if (File && FormData && textParamsByFieldName) {
  651. textParamsByFieldName = _.extend({}, textParamsByFieldName);
  652. _.each(textParamsByFieldName, function(value, fieldName){
  653. if (_.isObject(value) && value instanceof File) {
  654. filesByFieldName[fieldName] = value;
  655. delete textParamsByFieldName[fieldName];
  656. }
  657. });
  658. }//fi
  659. // Don't allow file uploads for GET requests,
  660. // or if the FormData constructor is somehow missing.
  661. if (_.keys(filesByFieldName).length > 0) {
  662. if (requestInfo.verb.match(/get/i)) {
  663. throw new Error(
  664. 'Detected File instance(s) provided for parameter(s): '+
  665. _.keys(filesByFieldName)+'\n'+
  666. 'But this is a nullipotent ('+requestInfo.verb.toUpperCase()+') '+
  667. 'request, which does not support file uploads.'
  668. );
  669. }//•
  670. if (!FormData) {
  671. throw new Error(
  672. 'Detected File instance(s) provided for parameter(s): '+
  673. _.keys(filesByFieldName)+'\n'+
  674. 'But the native FormData constructor does not exist!'
  675. );
  676. }
  677. }//fi
  678. switch (requestInfo.protocolName) {
  679. // ▄▄███▄▄· █████╗ ██╗ █████╗ ██╗ ██╗ ██╗██╗
  680. // ██╔════╝ ██╔══██╗ ██║██╔══██╗╚██╗██╔╝██╔╝╚██╗
  681. // ███████╗ ███████║ ██║███████║ ╚███╔╝ ██║ ██║
  682. // ╚════██║ ██╔══██║██ ██║██╔══██║ ██╔██╗ ██║ ██║
  683. // ███████║██╗██║ ██║╚█████╔╝██║ ██║██╔╝ ██╗╚██╗██╔╝
  684. // ╚═▀▀▀══╝╚═╝╚═╝ ╚═╝ ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝
  685. case 'jQuery': return (function _doAjaxWithJQuery(){
  686. var thisJQuery = requestInfo.protocolInstance;
  687. // Build options for $.ajax().
  688. var ajaxOpts = {
  689. url: requestInfo.url,
  690. method: requestInfo.verb
  691. };
  692. // If GET request, encode params in querystring.
  693. if (requestInfo.verb.match(/get/i)) {
  694. ajaxOpts.data = textParamsByFieldName;
  695. }
  696. // Else if there are files, attach them properly,
  697. // alongside the other stuff in the form.
  698. // > Note that we include text params **FIRST**,
  699. // > in order to support order-aware body parsers
  700. // > that rely on pessimistic upstream awareness
  701. // > optimize uploads and prevent DDoS attacks.
  702. else if (_.keys(filesByFieldName).length > 0){
  703. ajaxOpts.processData = false;
  704. ajaxOpts.contentType = false;
  705. ajaxOpts.data = new FormData();
  706. _.each(textParamsByFieldName, function(value, fieldName){
  707. // Skip `undefined` values to more accurately mirror
  708. // the behavior of JSON.stringify()
  709. if (value === undefined) { return; }
  710. ajaxOpts.data.append(fieldName, value);
  711. });
  712. _.each(filesByFieldName, function(file, fieldName){
  713. // Skip `undefined` values for consistency.
  714. if (file === undefined) { return; }
  715. if (!_.isObject(file) || !_.isObject(file.constructor) || file.constructor.name !== 'File') {
  716. throw new Error('Cannot upload as '+fieldName+' because the provided value is not a File instance. Instead, got:'+file);
  717. }
  718. ajaxOpts.data.append(fieldName, file, file.name);
  719. });
  720. }
  721. // Otherwise, attach params as a JSON-encoded request body.
  722. else {
  723. ajaxOpts.data = JSON.stringify(textParamsByFieldName);
  724. ajaxOpts.processData = false;
  725. ajaxOpts.contentType = 'application/json; charset=UTF-8';
  726. }
  727. if (typeof requestInfo.headers !== 'undefined') {
  728. ajaxOpts.headers = requestInfo.headers;
  729. }
  730. // Dealing with jqXHR:
  731. //
  732. // To get status code:
  733. // console.log(jqXHR.statusCode);
  734. //
  735. // To get header(s):
  736. // console.log(jqXHR.getResponseHeader('foo'));
  737. // - or -
  738. // console.log(jqXHR.getAllResponseHeaders());
  739. // ^^^ but this one gives it to you as a string.
  740. // ^^
  741. // WARNING: if using a cross-domain request w/ CORS, this (^^^^^)
  742. // header grabbing may not work properly on some versions of firefox. More details:
  743. // http://stackoverflow.com/questions/5614735/jqxhr-getallresponseheaders-wont-return-all-headers
  744. thisJQuery.ajax(_.extend(ajaxOpts, {
  745. error: function (jqXHR) {
  746. return proceed(undefined, {
  747. body: jqXHR.responseJSON === undefined ? jqXHR.responseText : jqXHR.responseJSON,
  748. statusCode: jqXHR.status,
  749. headers: _.reduce(jqXHR.getAllResponseHeaders().split(/\n/), function (memo, pair) {
  750. var splitPair = pair.split(/:/);
  751. var headerName = splitPair[0];
  752. if (headerName === '') { return memo; }
  753. // Note that we trim leading AND trailing whitespace.
  754. var headerVal = splitPair.slice(1).join('').replace(/^\s*/, '').replace(/\s*$/, '');
  755. memo[headerName] = headerVal;
  756. // Also add an alias using the all-lowercased version of the header name
  757. // (if it's different)
  758. var allLowercaseHeaderName = headerName.toLowerCase();
  759. if (allLowercaseHeaderName !== headerName) {
  760. memo[allLowercaseHeaderName] = headerVal;
  761. }
  762. return memo;
  763. }, {})
  764. });
  765. },
  766. success: function (unused0, unused1, jqXHR) {
  767. return proceed(undefined, {
  768. body: jqXHR.responseJSON === undefined ? jqXHR.responseText : jqXHR.responseJSON,
  769. statusCode: jqXHR.status,
  770. headers: _.reduce(jqXHR.getAllResponseHeaders().split(/\n/), function (memo, pair) {
  771. var splitPair = pair.split(/:/);
  772. var headerName = splitPair[0];
  773. if (headerName === '') { return memo; }
  774. // Note that we trim leading AND trailing whitespace.
  775. var headerVal = splitPair.slice(1).join('').replace(/^\s*/, '').replace(/\s*$/, '');
  776. memo[headerName] = headerVal;
  777. // Also add an alias using the all-lowercased version of the header name
  778. // (if it's different)
  779. var allLowercaseHeaderName = headerName.toLowerCase();
  780. if (allLowercaseHeaderName !== headerName) {
  781. memo[allLowercaseHeaderName] = headerVal;
  782. }
  783. return memo;
  784. }, {})
  785. });
  786. }
  787. }));//</ thisJQuery.ajax + _.extend() >
  788. })();//</self-calling function :: _doAjaxWithJQuery>
  789. // ██╗ ██████╗ ███████╗ ██████╗ ██████╗██╗ ██╗███████╗████████╗ ██╗██╗
  790. // ██║██╔═══██╗ ██╔════╝██╔═══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝▄ ██╗▄██╔╝╚██╗
  791. // ██║██║ ██║ ███████╗██║ ██║██║ █████╔╝ █████╗ ██║ ████╗██║ ██║
  792. // ██║██║ ██║ ╚════██║██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ▀╚██╔▀██║ ██║
  793. // ██║╚██████╔╝██╗███████║╚██████╔╝╚██████╗██║ ██╗███████╗ ██║██╗ ╚═╝ ╚██╗██╔╝
  794. // ╚═╝ ╚═════╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝╚═╝ ╚═╝╚═╝
  795. //
  796. case 'io.socket': return (function _doAjaxWithSocket(){
  797. var socket = requestInfo.protocolInstance;
  798. // If `File` constructor is available, check to be sure
  799. // that none of the parameter values are File instances.
  800. // > Note that if the File constructor is NOT available,
  801. // > then we don't even bother checking (it's not like it
  802. // > would work anyway!)
  803. if (File && requestInfo.params) {
  804. _.each(requestInfo.params, function(value, fieldName){
  805. if (_.isObject(value) && value instanceof File) {
  806. 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\'`)');
  807. }
  808. });
  809. }//fi
  810. // Determine if the socket has been disconnected, or if it
  811. // has NEVER BEEN connected and is not CURRENTLY TRYING to
  812. // connect.
  813. var disconnectedOrWasNeverConnectedAndUnlikelyToTry =
  814. // =>
  815. // If the socket is connected, cool, no problem.
  816. !socket.isConnected() &&
  817. // =>
  818. // If the socket is at least _attempting_ to connect, we'll go ahead
  819. // and let it try to do it's thing (i.e. queue and replay)
  820. !socket.isConnecting() &&
  821. // =>
  822. // If the socket hasn't even had the _chance_ to begin connecting
  823. // (because the one-tick auto-connect timer hasn't fired yet),
  824. // then we'll give it that chance.
  825. !socket.mightBeAboutToAutoConnect();
  826. // If none of the above were true, then emulate a normal
  827. // offline AJAX response from jQuery.
  828. if (disconnectedOrWasNeverConnectedAndUnlikelyToTry) {
  829. return proceed(undefined, {
  830. body: null,
  831. statusCode: 0,
  832. headers: {}
  833. });
  834. }
  835. // Otherwise the socket is either connected, in the process of connecting,
  836. // or in an indeterminate state where it has _never_ connected but _might_
  837. // still connect (see above for details).
  838. //
  839. // In any of these cases, thanks largely to queuing, it is safe to continue
  840. // onwards, and to send the request!
  841. socket.request({
  842. method: requestInfo.verb,
  843. url: requestInfo.url,
  844. data: requestInfo.params,
  845. headers: requestInfo.headers
  846. }, function (unused, jwres) {
  847. return proceed(undefined, {
  848. body: jwres.body,
  849. statusCode: jwres.statusCode,
  850. headers: jwres.headers
  851. });
  852. });//</ socket.request() >
  853. })();//</self-calling function :: _doAjaxWithSocket>
  854. // ███╗ ███╗██████╗ ██╗ ██╗████████╗████████╗██████╗
  855. // ████╗ ████║██╔══██╗ ██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗
  856. // ██╔████╔██║██████╔╝█████╗███████║ ██║ ██║ ██████╔╝
  857. // ██║╚██╔╝██║██╔═══╝ ╚════╝██╔══██║ ██║ ██║ ██╔═══╝
  858. // ██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║ ██║
  859. // ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
  860. //
  861. case 'machinepack-http': return (function _doAjaxWithMpHttp(){
  862. // If `File` constructor is available, check to be sure
  863. // that none of the parameter values are File instances.
  864. // > Note that if the File constructor is NOT available,
  865. // > then we don't even bother checking (it's not like it
  866. // > would work anyway!)
  867. if (File && requestInfo.params) {
  868. _.each(requestInfo.params, function(value, fieldName){
  869. if (_.isObject(value) && value instanceof File) {
  870. 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.');
  871. }
  872. });
  873. }//fi
  874. var mpHttpOpts = {
  875. url: requestInfo.url,
  876. method: requestInfo.verb
  877. };
  878. // If GET request, encode params in querystring.
  879. if (requestInfo.verb.match(/get/i)) {
  880. mpHttpOpts.qs = textParamsByFieldName;
  881. }
  882. // Otherwise, attach params as the request body.
  883. // (it will be JSON-encoded automatically by default)
  884. else {
  885. mpHttpOpts.body = textParamsByFieldName;
  886. }
  887. if (typeof requestInfo.headers !== 'undefined') {
  888. mpHttpOpts.headers = requestInfo.headers;
  889. }
  890. requestInfo.protocolInstance.sendHttpRequest(mpHttpOpts)
  891. .switch({
  892. error: function (err) {
  893. return proceed(err);
  894. },
  895. requestFailed: function(err) {
  896. return proceed(undefined, {
  897. body: err.message,
  898. statusCode: 0,
  899. headers: {}
  900. });
  901. },
  902. non200Response: function(serverResponse) {
  903. return proceed(undefined, serverResponse);
  904. },
  905. success: function (serverResponse){
  906. // If there is no response body (i.e. `body` is `""`),
  907. // then we'll interpret that as `null` and return that as
  908. // our response data.
  909. if (serverResponse.body === '') {
  910. serverResponse.body = null;
  911. }
  912. // --•
  913. // Otherwise, attempt to parse the response body as JSON.
  914. try {
  915. serverResponse.body = JSON.parse(serverResponse.body);
  916. } catch (err) {//eslint-disable-line no-unused-vars
  917. // If the raw response body string cannot be parsed as JSON,
  918. // then interpret it as a string by leaving the raw body as-is.
  919. }
  920. return proceed(undefined, serverResponse);
  921. }
  922. });//_∏_
  923. })();//</self-calling function :: _doAjaxWithMpHttp>
  924. default:
  925. throw new Error('Consistency violation: Unexpected protocol name received (`'+requestInfo.protocolName+'`)-- but it should have already been checked!');
  926. }//</switch(protocol)>
  927. })(function afterwards(err, responseInfo){
  928. if (err) {
  929. throw new Error('Consistency violation: Unexpected error in CloudSDK. Details: '+err.stack);
  930. }
  931. // Note that the response info dictionary is intended to be
  932. // a transport-agnostic way of representing a server response.
  933. // (similar to jQuery AJAX response objects / jqXHR / jwRes):
  934. // --------------------------------------------------------------------
  935. // AVAILABLE AT THIS POINT:
  936. // • responseInfo.statusCode
  937. // • responseInfo.body
  938. // • responseInfo.headers
  939. // --------------------------------------------------------------------
  940. // ATTACHED BELOW:
  941. // • responseInfo.data << for compatibility
  942. // • responseInfo.exit << either `success`, `error`, or the code name of some other exit.
  943. // • responseInfo.code << (alias for "exit")
  944. // --------------------------------------------------------------------
  945. // To get exit info:
  946. // console.log(responseInfo.headers['X-Exit']);
  947. // console.log(responseInfo.headers['X-Exit-FriendlyName']);
  948. // console.log(responseInfo.headers['X-Exit-Description']);
  949. // console.log(responseInfo.headers['X-Exit-Extended-Description']);
  950. // console.log(responseInfo.headers['X-Exit-Output-Friendly-Name']);
  951. // console.log(responseInfo.headers['X-Exit-Output-Description']);
  952. // COMPATIBILITY:
  953. // Stick `data` property on responseInfo so it feels familiar
  954. // e.g. like `res.data` in angular 1
  955. // (This is mainly for backwards compatibility, and can probably
  956. // be removed at some point.)
  957. if (!_.isUndefined(responseInfo.body)) {
  958. responseInfo.data = responseInfo.body;
  959. }
  960. // Determine the appropriate callback to call.
  961. //
  962. // We also stick on `exit` property for convenience
  963. // by sniffing the X-Exit header. We default to `error`
  964. // or `success`, depending on whether an Error instance
  965. // was passed through as the `error` property.
  966. var xExitResponseHeaderValue = responseInfo.headers['x-exit'] || responseInfo.headers['X-Exit'];
  967. // ^This "either-or-ing" is likely necessary because of different jQuery versions.
  968. if (xExitResponseHeaderValue === '_offline') {
  969. 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.');
  970. }//fi
  971. // If the user's computer is offline or the server is down, etc...
  972. // > If `statusCode` is 0, then the user is probably offline.
  973. // > Or maybe our server is down omg.
  974. // > Or it could be that a cross-origin request was blocked.
  975. if (responseInfo.statusCode === 0) {
  976. responseInfo.exit = '_offline';
  977. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  978. // FUTURE: Instead of "_offline", support more granular/accurate
  979. // built-in exits like:
  980. // • '_failedCrossOriginRequest'
  981. // • '_serverDown'
  982. // • '_clientOffline'
  983. //
  984. // ^^In the future, if we want to get really fancy,
  985. // we could try sending a ping to another CORS-enabled
  986. // endpoint to see whether it's us or them. Not sure
  987. // how we'd figure out if it's a failed cross-origin
  988. // request though...
  989. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  990. }
  991. // If the server responded with a specific error...
  992. else if (xExitResponseHeaderValue){
  993. responseInfo.exit = xExitResponseHeaderValue;
  994. }
  995. // If the server responded with some other misc. error...
  996. else if (responseInfo.statusCode < 200 || responseInfo.statusCode >= 300) {
  997. responseInfo.exit = 'error';
  998. }
  999. // Otherwise, we'll consider it a success!
  1000. else {
  1001. responseInfo.exit = 'success';
  1002. }
  1003. // Set up `code` as alias for `exit`, for consistency.
  1004. responseInfo.code = responseInfo.exit;
  1005. // Now before proceeding further, check lifecycleInstructions for a match (if there are any configured).
  1006. // > NOTE: We only ever run one of these handlers for any given response!
  1007. var matchingLifecycleInstruction = _.find(requestInfo.lifecycleInstructions, function(lifecycleInstruction) {
  1008. if (lifecycleInstruction.rule === undefined) {
  1009. if (responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
  1010. return false;
  1011. }
  1012. else {
  1013. return true;
  1014. }
  1015. }
  1016. else if (responseInfo.statusCode === lifecycleInstruction.rule) {
  1017. return true;
  1018. }
  1019. else if (responseInfo.exit === lifecycleInstruction.rule) {
  1020. return true;
  1021. }
  1022. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1023. // FUTURE: add support for bluebird style dictionary rules
  1024. // (see flaverr.taste at https://npmjs.com/package/flaverr)
  1025. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1026. });//∞
  1027. // If there was a match, then run this intercept/toleration's handler function.
  1028. (function _runInterceptOrTolerationMaybe(proceed){
  1029. if (!matchingLifecycleInstruction) {
  1030. return proceed();
  1031. }//•
  1032. var resultFromHandler;
  1033. if (matchingLifecycleInstruction.handler.constructor.name === 'AsyncFunction') {
  1034. return proceed(new Error('`async` functions are not *yet* fully supported in intercept/tolerate'));
  1035. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1036. // FUTURE: add support for this, beginning with something like the
  1037. // following incomplete implementation:
  1038. //
  1039. // ```
  1040. // var interceptPromise;
  1041. // try {
  1042. // interceptPromise = matchingLifecycleInstruction.handler();
  1043. // } catch (err) {
  1044. // if (err === false) { return proceed(undefined, true); }//« special case (`throw false`)
  1045. // else { return proceed(err); }
  1046. // }
  1047. //
  1048. // interceptPromise.then(function(_resultFromHandler){
  1049. // resultFromHandler = _resultFromHandler;
  1050. // proceed(undefined, resultFromHandler);
  1051. // });
  1052. // interceptPromise.catch(function(err) {
  1053. // /* eslint-disable callback-return */
  1054. // if (err === false) { proceed(undefined, true); }//« special case (`throw false`)
  1055. // else { proceed(err); }
  1056. // /* eslint-enable callback-return */
  1057. // });
  1058. // ```
  1059. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1060. }
  1061. else {
  1062. try {
  1063. // FUTURE: do this addition of properties earlier:
  1064. errorInstance.exit = responseInfo.exit;
  1065. errorInstance.code = responseInfo.exit;
  1066. errorInstance.responseInfo = responseInfo;
  1067. resultFromHandler = matchingLifecycleInstruction.handler(errorInstance);
  1068. } catch (err) {
  1069. if (err === false) { return proceed(undefined, true); }//« special case (`throw false`)
  1070. else { return proceed(err); }
  1071. }
  1072. return proceed(undefined, resultFromHandler);
  1073. }
  1074. })(function(err, resultFromInterceptOrTolerate) {
  1075. if (err) {
  1076. throw new Error('The provided custom intercept/tolerate logic threw an unexpected, uncaught error: '+err.stack);
  1077. // FUTURE: better error handling for this case ^^
  1078. }//•
  1079. if (matchingLifecycleInstruction) {
  1080. if (responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
  1081. throw new Error('Unexpected intercept/tolerate logic matched a 2xx/success response, but these methods should only be used for exceptions!');
  1082. // FUTURE: better error handling for this case ^^
  1083. }
  1084. }
  1085. // If a matching `.tolerate()` was encountered, then consider this successful no matter what.
  1086. var tolerateAsIfSuccess = (matchingLifecycleInstruction && matchingLifecycleInstruction.type === 'tolerate');
  1087. // If a matching `.intercept()` was encountered, then consider whatever the intercept handler
  1088. // returned to be our new Error.
  1089. if (matchingLifecycleInstruction && matchingLifecycleInstruction.type === 'intercept') {
  1090. if (!_.isError(resultFromInterceptOrTolerate)) {
  1091. throw new Error('Unexpected value returned from .intercept() handler. Expected an Error instance but instead, got: '+resultFromInterceptOrTolerate);
  1092. // FUTURE: better error handling for this case ^^
  1093. }
  1094. errorInstance = resultFromInterceptOrTolerate;
  1095. }
  1096. // If no custom error callback was specified, but we don't know
  1097. // how to handle an error below, then we'll simply throw a fatal
  1098. // error (this can be caught by `window.onerror` et. al. in order
  1099. // to trigger a fatal error message, e.g. using a devoted user
  1100. // interface component as a global error bus.)
  1101. //
  1102. // This string is used as a prefix for the various error messages
  1103. // that can occur this way throughout the code below.
  1104. var UNHANDLED_ERR_PREFIX_MSG =
  1105. (
  1106. (responseInfo.statusCode===0)?
  1107. 'Unable to send request... are the client and server both online?'
  1108. :'Received unhandled '+responseInfo.statusCode+' error from server.'
  1109. )+' '+
  1110. '(See `responseInfo` property of this error for details). '+
  1111. 'Note that you can negotiate any error using its `exit` or `responseInfo.statusCode` properties.\n'+
  1112. '--\n';
  1113. // ┌─┐┌─┐┌┐┌┌─┐┬─┐┬┌─┐ ┌─┐─┐ ┬┌─┐┌─┐ ┌─┐┌─┐┬ ┬ ┌┐ ┌─┐┌─┐┬┌─ ┌─┐┬ ┬┌┐┌┌─┐┌┬┐┬┌─┐┌┐┌
  1114. // │ ┬├┤ │││├┤ ├┬┘││ ├┤ ┌┴┬┘├┤ │ │ ├─┤│ │ ├┴┐├─┤│ ├┴┐ ├┤ │ │││││ │ ││ ││││
  1115. // └─┘└─┘┘└┘└─┘┴└─┴└─┘ o└─┘┴ └─└─┘└─┘ └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴ └ └─┘┘└┘└─┘ ┴ ┴└─┘┘└┘
  1116. // If a generic callback was provided...
  1117. if (_.isFunction(exitCallbacks)) {
  1118. if (tolerateAsIfSuccess) {
  1119. return exitCallbacks(undefined, resultFromInterceptOrTolerate, responseInfo);
  1120. }
  1121. else if (responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
  1122. return exitCallbacks(undefined, responseInfo.body, responseInfo);
  1123. }
  1124. else {
  1125. errorInstance.stack += '\n'+
  1126. '\n'+
  1127. 'Error Summary:\n'+
  1128. '(see `.responseInfo` for more details)\n'+
  1129. '·-------------·----------------------------------------·\n'+
  1130. '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
  1131. '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
  1132. '| Exit | '+responseInfo.exit+'\n'+
  1133. '| Status Code | '+responseInfo.statusCode+'\n'+
  1134. '·-------------·----------------------------------------·';
  1135. if (responseInfo.body !== undefined) {
  1136. errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
  1137. }
  1138. errorInstance.responseInfo = responseInfo;
  1139. errorInstance.exit = responseInfo.exit;
  1140. errorInstance.code = responseInfo.exit;
  1141. return exitCallbacks(errorInstance, responseInfo.body!==undefined?responseInfo.body:errorInstance, responseInfo);
  1142. }
  1143. }//‡
  1144. // ┌─┐┬ ┬┬┌┬┐┌─┐┬ ┬┌┐ ┌─┐┌─┐┬┌─ ┌┬┐┬┌─┐┌┬┐┬┌─┐┌┐┌┌─┐┬─┐┬ ┬
  1145. // └─┐││││ │ │ ├─┤├┴┐├─┤│ ├┴┐ ││││ │ ││ ││││├─┤├┬┘└┬┘
  1146. // └─┘└┴┘┴ ┴ └─┘┴ ┴└─┘┴ ┴└─┘┴ ┴ ─┴┘┴└─┘ ┴ ┴└─┘┘└┘┴ ┴┴└─ ┴
  1147. // If a dictionary of callbacks was provided...
  1148. else if (_.isObject(exitCallbacks)) {
  1149. // If this isn't the error exit, and a callback exists for it...
  1150. if (responseInfo.exit !== 'error' && exitCallbacks[responseInfo.exit]) {
  1151. // If there's a response body, pass it to the callback.
  1152. if (responseInfo.body !== undefined) {
  1153. return exitCallbacks[responseInfo.exit](tolerateAsIfSuccess ? resultFromInterceptOrTolerate : responseInfo.body, responseInfo);
  1154. }
  1155. // Otherwise, there's no response body.
  1156. // So if this is a "success" response, or any 2xx response,
  1157. // then don't pass a first arg to the callback.
  1158. else if (tolerateAsIfSuccess || responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
  1159. return exitCallbacks['success'](tolerateAsIfSuccess ? resultFromInterceptOrTolerate : undefined, responseInfo);
  1160. }
  1161. // Otherwise, pass an error instance as the first arg of the callback.
  1162. else {
  1163. errorInstance.stack += '\n'+
  1164. '\n'+
  1165. 'Error Summary:\n'+
  1166. '(see `.responseInfo` for more details)\n'+
  1167. '·-------------·----------------------------------------·\n'+
  1168. '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
  1169. '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
  1170. '| Exit | '+responseInfo.exit+'\n'+
  1171. '| Status Code | '+responseInfo.statusCode+'\n'+
  1172. '·-------------·----------------------------------------·';
  1173. if (responseInfo.body !== undefined) {
  1174. errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
  1175. }
  1176. errorInstance.responseInfo = responseInfo;
  1177. errorInstance.exit = responseInfo.exit;
  1178. errorInstance.code = responseInfo.exit;
  1179. return exitCallbacks[responseInfo.exit](errorInstance, responseInfo);
  1180. }
  1181. }
  1182. // Otherwise, if this is a "success" response, or any 2xx response, then...
  1183. else if (tolerateAsIfSuccess || responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
  1184. if (exitCallbacks['success']) {
  1185. // Either forward to the "success" callback (if there is one)
  1186. return exitCallbacks['success'](tolerateAsIfSuccess ? resultFromInterceptOrTolerate : responseInfo.body, responseInfo);
  1187. }
  1188. else {
  1189. // or otherwise do nothing.
  1190. }
  1191. }
  1192. // Otherwise call the error callback.
  1193. else if (exitCallbacks['error']) {
  1194. errorInstance.stack += '\n'+
  1195. '\n'+
  1196. 'Error Summary:\n'+
  1197. '(see `.responseInfo` for more details)\n'+
  1198. '·-------------·----------------------------------------·\n'+
  1199. '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
  1200. '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
  1201. '| Exit | '+responseInfo.exit+'\n'+
  1202. '| Status Code | '+responseInfo.statusCode+'\n'+
  1203. '·-------------·----------------------------------------·';
  1204. if (responseInfo.body !== undefined) {
  1205. errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
  1206. }
  1207. errorInstance.responseInfo = responseInfo;
  1208. errorInstance.exit = responseInfo.exit;
  1209. errorInstance.code = responseInfo.code;
  1210. return exitCallbacks['error'](errorInstance, responseInfo);
  1211. }
  1212. // Or if there isn't an error callback, just throw.
  1213. else {
  1214. errorInstance.stack += '\n'+
  1215. '\n'+
  1216. 'Error Summary:\n'+
  1217. '(see `.responseInfo` for more details)\n'+
  1218. '·-------------·----------------------------------------·\n'+
  1219. '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
  1220. '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
  1221. '| Exit | '+responseInfo.exit+'\n'+
  1222. '| Status Code | '+responseInfo.statusCode+'\n'+
  1223. '·-------------·----------------------------------------·';
  1224. if (responseInfo.body !== undefined) {
  1225. errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
  1226. }
  1227. errorInstance.stack = UNHANDLED_ERR_PREFIX_MSG + errorInstance.stack;
  1228. errorInstance.responseInfo = responseInfo;
  1229. throw errorInstance;
  1230. }
  1231. }//‡
  1232. // ┌┐┌┌─┐ ┌─┐┌─┐┬ ┬ ┌┐ ┌─┐┌─┐┬┌─ ┌─┐┌─┐ ┌─┐┌┐┌┬ ┬ ┬┌─┬┌┐┌┌┬┐
  1233. // ││││ │ │ ├─┤│ │ ├┴┐├─┤│ ├┴┐ │ │├┤ ├─┤│││└┬┘ ├┴┐││││ ││
  1234. // ┘└┘└─┘ └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴ └─┘└ ┴ ┴┘└┘ ┴ ┴ ┴┴┘└┘─┴┘
  1235. // If _no callbacks of any kind_ were provided...
  1236. else if (_.isUndefined(exitCallbacks)) {
  1237. // If this was successful, then do nothing.
  1238. if (tolerateAsIfSuccess || responseInfo.exit === 'success' || (responseInfo.statusCode >= 200 && responseInfo.statusCode < 300)) {
  1239. return;
  1240. }
  1241. // Otherwise, throw.
  1242. else {
  1243. errorInstance.stack += '\n'+
  1244. '\n'+
  1245. 'Error Summary:\n'+
  1246. '(see `.responseInfo` for more details)\n'+
  1247. '·-------------·----------------------------------------·\n'+
  1248. '| Protocol | '+(requestInfo.protocolName==='jQuery'?'http(s):// (jQuery)':requestInfo.protocolName==='io.socket'?'ws(s):// (io.socket)':requestInfo.protocolName)+'\n'+
  1249. '| Address | '+requestInfo.verb.toUpperCase()+' '+requestInfo.url+'\n'+
  1250. '| Exit | '+responseInfo.exit+'\n'+
  1251. '| Status Code | '+responseInfo.statusCode+'\n'+
  1252. '·-------------·----------------------------------------·';
  1253. if (responseInfo.body !== undefined) {
  1254. errorInstance.stack += '\n\nResponse Body:\n'+responseInfo.body;
  1255. }
  1256. errorInstance.stack = UNHANDLED_ERR_PREFIX_MSG + errorInstance.stack;
  1257. errorInstance.responseInfo = responseInfo;
  1258. throw errorInstance;
  1259. }
  1260. }//‡
  1261. else {
  1262. throw new Error('Invalid usage of Cloud.*() method. Provide either a dictionary of callbacks, a single callback function, or NOTHING to `.exec()`.');
  1263. }
  1264. });//_∏_ </cb from † _runInterceptOrTolerationMaybe()>
  1265. });//_∏_ </cb from † _makeAjaxCallWithAppropriateProtocol()>
  1266. // --
  1267. // > Note that we don't return anything at all here.
  1268. // > (That's to ensure userland code doesn't attempt any further chaining or `await`ing.)
  1269. },//</definition of `.exec()` >
  1270. switch: function (){
  1271. deferred.exec.apply(deferred, arguments);
  1272. // --
  1273. // > Note that we don't return anything at all here.
  1274. // > (That's to ensure userland code doesn't attempt any further chaining or `await`ing.)
  1275. },
  1276. // FUTURE: use parley for this instead, if available
  1277. log: function (){
  1278. console.log('Running with `.log()`...');
  1279. this.exec(function(err, result) {
  1280. if (err) {
  1281. console.error();
  1282. console.error('- - - - - - - - - - - - - - - - - - - - - - - -');
  1283. console.error('An error occurred:');
  1284. console.error();
  1285. console.error(err);
  1286. console.error('- - - - - - - - - - - - - - - - - - - - - - - -');
  1287. console.error();
  1288. return;
  1289. }//-•
  1290. console.log();
  1291. if (_.isUndefined(result)) {
  1292. console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
  1293. console.log('Finished successfully.');
  1294. console.log();
  1295. console.log('(There was no result.)');
  1296. console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
  1297. }
  1298. else {
  1299. console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
  1300. console.log('Finished successfully.');
  1301. console.log();
  1302. console.log('Result:');
  1303. console.log();
  1304. console.log(result);
  1305. console.log('- - - - - - - - - - - - - - - - - - - - - - - -');
  1306. }
  1307. console.log();
  1308. });//_∏_
  1309. // --
  1310. // > Note that we don't return anything at all here.
  1311. // > (That's to ensure userland code doesn't attempt any further chaining or `await`ing.)
  1312. }
  1313. };// </define deferred object>
  1314. // ███╗ ███╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ███████╗███████╗ █████╗ ██╗ ██╗██╗ ████████╗███████╗
  1315. // ████╗ ████║██╔════╝██╔══██╗██╔════╝ ██╔════╝ ██╔══██╗██╔════╝██╔════╝██╔══██╗██║ ██║██║ ╚══██╔══╝██╔════╝
  1316. // ██╔████╔██║█████╗ ██████╔╝██║ ███╗█████╗ ██║ ██║█████╗ █████╗ ███████║██║ ██║██║ ██║ ███████╗
  1317. // ██║╚██╔╝██║██╔══╝ ██╔══██╗██║ ██║██╔══╝ ██║ ██║██╔══╝ ██╔══╝ ██╔══██║██║ ██║██║ ██║ ╚════██║
  1318. // ██║ ╚═╝ ██║███████╗██║ ██║╚██████╔╝███████╗ ██████╔╝███████╗██║ ██║ ██║╚██████╔╝███████╗██║ ███████║
  1319. // ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝
  1320. //
  1321. // ███████╗██████╗ ██████╗ ███╗ ███╗ ███████╗███╗ ██╗██████╗ ██████╗ ██████╗ ██╗███╗ ██╗████████╗
  1322. // ██╔════╝██╔══██╗██╔═══██╗████╗ ████║ ██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔═══██╗██║████╗ ██║╚══██╔══╝
  1323. // █████╗ ██████╔╝██║ ██║██╔████╔██║ █████╗ ██╔██╗ ██║██║ ██║██████╔╝██║ ██║██║██╔██╗ ██║ ██║
  1324. // ██╔══╝ ██╔══██╗██║ ██║██║╚██╔╝██║ ██╔══╝ ██║╚██╗██║██║ ██║██╔═══╝ ██║ ██║██║██║╚██╗██║ ██║
  1325. // ██║ ██║ ██║╚██████╔╝██║ ╚═╝ ██║ ███████╗██║ ╚████║██████╔╝██║ ╚██████╔╝██║██║ ╚████║ ██║
  1326. // ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝
  1327. //
  1328. // ██████╗ ███████╗███████╗██╗███╗ ██╗██╗████████╗██╗ ██████╗ ███╗ ██╗
  1329. // ██╔══██╗██╔════╝██╔════╝██║████╗ ██║██║╚══██╔══╝██║██╔═══██╗████╗ ██║
  1330. // ██║ ██║█████╗ █████╗ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║
  1331. // ██║ ██║██╔══╝ ██╔══╝ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║
  1332. // ██████╔╝███████╗██║ ██║██║ ╚████║██║ ██║ ██║╚██████╔╝██║ ╚████║
  1333. // ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
  1334. //
  1335. // Now set up the endpoint definition.
  1336. //////////////////////////////////////////////////////////////////////
  1337. // If a function was supplied, call it and use the dictionary
  1338. // it returns as our request info.
  1339. //////////////////////////////////////////////////////////////////////
  1340. // e.g. function (argins){
  1341. // return {
  1342. // verb: 'post',
  1343. // url: '/foo/bar',
  1344. // params: {
  1345. // whatever: 'you want' + ' in here maybe using '+argins.whatever
  1346. // },
  1347. // headers: {}
  1348. // }
  1349. // }
  1350. if (typeof appLevelSdkEndpointDef === 'function') {
  1351. var returnedFromEndpointDefFn = appLevelSdkEndpointDef.apply(this, argins);
  1352. if (typeof returnedFromEndpointDefFn !== 'object') {
  1353. 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.');
  1354. }
  1355. if (!_.isUndefined(returnedFromEndpointDefFn.headers)) {
  1356. deferred = deferred.headers(returnedFromEndpointDefFn.headers);
  1357. }
  1358. else if (options.headers) {
  1359. deferred = deferred.headers(options.headers);
  1360. }
  1361. if (!_.isUndefined(returnedFromEndpointDefFn.protocol)) {
  1362. deferred = deferred.protocol(returnedFromEndpointDefFn.protocol);
  1363. }
  1364. else if (options.protocol) {
  1365. deferred.protocol(options.protocol);
  1366. }
  1367. else { deferred.protocol(DEFAULT_PROTOCOL_NAME); }
  1368. requestInfo.verb = returnedFromEndpointDefFn.verb;
  1369. requestInfo.url = returnedFromEndpointDefFn.url;
  1370. requestInfo.params = argins;
  1371. }
  1372. // If a dictionary was supplied, use that as our request info.
  1373. //////////////////////////////////////////////////////////////////////
  1374. // e.g. {
  1375. // verb: 'post',
  1376. // url: '/foo/bar',
  1377. // protocol: 'io.socket',//optional, defaults to 'jQuery'
  1378. // headers: {'x-auth': 'foo'},//optional, defaults to undefined
  1379. // }
  1380. else if (appLevelSdkEndpointDef && typeof appLevelSdkEndpointDef === 'object') {
  1381. if (!_.isUndefined(appLevelSdkEndpointDef.headers)) {
  1382. deferred.headers(appLevelSdkEndpointDef.headers);
  1383. }
  1384. else if (options.headers) {
  1385. deferred = deferred.headers(options.headers);
  1386. }
  1387. if (!_.isUndefined(appLevelSdkEndpointDef.protocol)) {
  1388. deferred.protocol(appLevelSdkEndpointDef.protocol);
  1389. }
  1390. else if (options.protocol) {
  1391. deferred.protocol(options.protocol);
  1392. }
  1393. else { deferred.protocol(DEFAULT_PROTOCOL_NAME); }
  1394. requestInfo.verb = appLevelSdkEndpointDef.verb;
  1395. requestInfo.url = appLevelSdkEndpointDef.url;
  1396. requestInfo.params = argins;
  1397. }
  1398. // If a string was supplied, expand and use that as our request info.
  1399. //////////////////////////////////////////////////////////////////////
  1400. // e.g. "POST /api/v1/lawnmowers/foo/inputs/bar"
  1401. else if (typeof appLevelSdkEndpointDef === 'string') {
  1402. if (options.headers) {
  1403. deferred = deferred.headers(options.headers);
  1404. }
  1405. // Set up default protocol.
  1406. if (options.protocol) {
  1407. deferred.protocol(options.protocol);
  1408. }
  1409. else { deferred.protocol(DEFAULT_PROTOCOL_NAME); }
  1410. // And then fold in the other pieces of request info.
  1411. requestInfo.verb = appLevelSdkEndpointDef.replace(/^\s*([^\/\s]+)\s*\/.*$/, '$1');
  1412. requestInfo.url = appLevelSdkEndpointDef.replace(/^\s*[^\/\s]+\s*\/(.*)$/, '/$1');
  1413. requestInfo.params = argins;
  1414. }
  1415. else {
  1416. 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.');
  1417. }
  1418. // Now template in URL pattern vars from the runtime request args.
  1419. /////////////////////////////////////////////////////////////////////////////
  1420. // Find keys in `params` which are route parameters
  1421. // (e.g. referenced by the endpoint URL)
  1422. // > Note that we're not actually interested in the return value
  1423. // > from this first `.replace()` here.
  1424. var routeParameters = {};
  1425. requestInfo.url.replace(/(\:[^\/\:\.\?]+\??)/g, function ($all, $1){
  1426. var routeParamName = $1.replace(/^\:/, '').replace(/\??$/, '');
  1427. // Optional:
  1428. if ($1.match(/\?$/)) {
  1429. if (requestInfo.params && requestInfo.params[routeParamName]) {
  1430. routeParameters[routeParamName] = requestInfo.params[routeParamName];
  1431. }
  1432. }
  1433. // Mandatory:
  1434. else {
  1435. if (!requestInfo.params || requestInfo.params[routeParamName] === undefined) {
  1436. throw new Error('Missing required param: `'+routeParamName+'`');
  1437. }
  1438. routeParameters[routeParamName] = requestInfo.params[routeParamName];
  1439. }
  1440. });//∞
  1441. // Then delete them from the `params` object
  1442. Object.keys(routeParameters).forEach(function (paramName){
  1443. delete requestInfo.params[paramName];
  1444. });
  1445. // Now stick the route parameters into the destination url
  1446. requestInfo.url = requestInfo.url.replace(/(\:[^\/\:\.\?]+\??)/g, function ($all, $1){
  1447. var routeParamName = $1.replace(/^\:/, '').replace(/\??$/, '');
  1448. if (routeParameters[routeParamName] === undefined) { return ''; }
  1449. return routeParameters[routeParamName];
  1450. });
  1451. // Prepend the API base URL to `requestInfo.url`.
  1452. /////////////////////////////////////////////////////////////////////////////
  1453. requestInfo.url = options.apiBaseUrl + requestInfo.url;
  1454. // Ensure verb exists, and then lower-case it.
  1455. /////////////////////////////////////////////////////////////////////////////
  1456. 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.)'); }
  1457. requestInfo.verb = (requestInfo.verb || 'get').toLowerCase();
  1458. // Attach the `requestInfo` as a property on the Deferred object itself, for easier integration
  1459. // with 3rd-party tools (e.g. autocomplete)
  1460. deferred.requestInfo = requestInfo;
  1461. // Return the deferred object.
  1462. return deferred;
  1463. };//ƒ </ _helpCallCloudMethod >
  1464. // Primary definition of this Cloud.* method()
  1465. memo[methodName] = function () {
  1466. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1467. // FUTURE: If no `args` configured, then check route params (url pattern
  1468. // variables). If there are any, attempt to use their names... maybe?
  1469. // Could actually be MORE confusing though-- needs to be played with.
  1470. //
  1471. // UPDATE: OK probably best not to do this actually.
  1472. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1473. if (!appLevelSdkEndpointDef.args && arguments.length > 0) {
  1474. throw new Error(
  1475. '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'+
  1476. ' [?] If you\'re unsure, visit https://sailsjs.com/support for help.'
  1477. );
  1478. }
  1479. // Parse arguments into argins
  1480. var argins = _.reduce(arguments, function(argins, argin, i){
  1481. if (!(appLevelSdkEndpointDef.args[i])) {
  1482. throw new Error('Invalid usage with serial arguments: Received unexpected '+(i===0?'first':i===1?'second':i===2?'third':(i+1)+'th')+' argument.');
  1483. }
  1484. // Reject special notation.
  1485. // > Remember, if we made it to this point, we know it's valid b/c it's already been checked.
  1486. if (appLevelSdkEndpointDef.args[i] === '{*}') {
  1487. if (argin !== undefined && (!_.isObject(argin) || _.isArray(argin) || _.isFunction(argin))) {
  1488. 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+'');
  1489. } else if (argin !== undefined && _.intersection(_.keys(argins), _.keys(argin)).length > 0) {
  1490. 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))+'');
  1491. }
  1492. _.extend(argins, argin);
  1493. } else {
  1494. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1495. // Note: For design considerations & historical context, see:
  1496. // • https://github.com/node-machine/machine/commit/fa3829fa637a267793be4a7fb573e008581c4656
  1497. // • https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facec
  1498. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1499. // FUTURE: Support declaring variadic usage
  1500. // https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facecR58
  1501. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1502. // FUTURE: Support declaring spread arguments
  1503. // https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facecR66
  1504. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1505. // Otherwise interpret this as the code name of an input
  1506. argins[appLevelSdkEndpointDef.args[i]] = argin;
  1507. }
  1508. return argins;
  1509. }, {});//= (∞)
  1510. return _helpCallCloudMethod(argins);
  1511. };//ƒ
  1512. // Escape hatch that always allows using named parameters.
  1513. memo[methodName].with = function (argins) {
  1514. return _helpCallCloudMethod(argins);
  1515. };//ƒ
  1516. return memo;
  1517. }, {});//</ _.reduce() :: each defined endpoint method >
  1518. // Remove the `.setup()` method, now that it's been called.
  1519. delete Cloud.setup;
  1520. // Now attach the methods
  1521. _.extend(Cloud, methods);
  1522. };
  1523. return Cloud;
  1524. });