UNPKG

traverson

Version:

Hypermedia API/HATEOAS client for Node.js and the browser

1,551 lines (1,373 loc) 250 kB
require=(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ 'use strict'; // TODO Replace by a proper lightweight logging module, suited for the browser var enabled = false; function Logger(id) { if (id == null) { id = ''; } this.id = id; } Logger.prototype.enable = function() { this.enabled = true; }; Logger.prototype.debug = function(message) { if (enabled) { console.log(this.id + '/debug: ' + message); } }; Logger.prototype.info = function(message) { if (enabled) { console.log(this.id + '/info: ' + message); } }; Logger.prototype.warn = function(message) { if (enabled) { console.log(this.id + '/warn: ' + message); } }; Logger.prototype.error = function(message) { if (enabled) { console.log(this.id + '/error: ' + message); } }; function minilog(id) { return new Logger(id); } minilog.enable = function() { enabled = true; }; module.exports = minilog; },{}],2:[function(require,module,exports){ 'use strict'; module.exports = { isArray: function(o) { if (o == null) { return false; } return Object.prototype.toString.call(o) === '[object Array]'; } }; },{}],3:[function(require,module,exports){ 'use strict'; var superagent = require('superagent'); function Request() {} Request.prototype.get = function(uri, options, callback) { return mapRequest(superagent.get(uri), options) .end(handleResponse(callback)); }; Request.prototype.post = function(uri, options, callback) { return mapRequest(superagent.post(uri), options) .end(handleResponse(callback)); }; Request.prototype.put = function(uri, options, callback) { return mapRequest(superagent.put(uri), options) .end(handleResponse(callback)); }; Request.prototype.patch = function(uri, options, callback) { return mapRequest(superagent.patch(uri), options) .end(handleResponse(callback)); }; Request.prototype.del = function(uri, options, callback) { return mapRequest(superagent.del(uri), options) .end(handleResponse(callback)); }; function mapRequest(superagentRequest, options) { options = options || {}; mapQuery(superagentRequest, options); mapHeaders(superagentRequest, options); mapAuth(superagentRequest, options); mapBody(superagentRequest, options); mapForm(superagentRequest, options); mapWithCredentials(superagentRequest, options); return superagentRequest; } function mapQuery(superagentRequest, options) { var qs = options.qs; if (qs != null) { superagentRequest = superagentRequest.query(qs); } } function mapHeaders(superagentRequest, options) { var headers = options.headers; if (headers != null) { superagentRequest = superagentRequest.set(headers); } } function mapAuth(superagentRequest, options) { var auth = options.auth; if (auth != null) { superagentRequest = superagentRequest.auth( auth.user || auth.username, auth.pass || auth.password ); } } function mapBody(superagentRequest, options) { if (options != null) { var body = options.body; if (body != null) { superagentRequest = superagentRequest.send(body); } } } function mapForm(superagentRequest, options) { if (options != null) { var form = options.form; if (form != null) { // content-type header needs to be set before calling send AND it NEEDS // to be all lower case otherwise superagent automatically sets // application/json as content-type :-/ superagentRequest = superagentRequest.set('content-type', 'application/x-www-form-urlencoded'); superagentRequest = superagentRequest.send(form); } } } function mapWithCredentials(superagentRequest, options) { if (options != null) { var withCredentials = options.withCredentials; if (withCredentials === true) { // https://visionmedia.github.io/superagent/#cors superagentRequest.withCredentials(); } } } // map XHR response object properties to Node.js request lib's response object // properties function mapResponse(response) { response.body = response.text; response.statusCode = response.status; return response; } function handleResponse(callback) { return function(err, response) { if (err) { if (!response) { // network error or timeout, no response return callback(err); } else { // Since 1.0.0 superagent calls the callback with an error if the status // code of the response is not in the 2xx range. In this cases, it also // passes in the response. To align things with request, call the // callback without the error but just with the response. callback(null, mapResponse(response)); } } else { callback(null, mapResponse(response)); } }; } module.exports = new Request(); },{"superagent":57}],4:[function(require,module,exports){ 'use strict'; /* * Copied from underscore.string module. Just the functions we need, to reduce * the browserified size. */ var _s = { startsWith: function(str, starts) { if (starts === '') return true; if (str == null || starts == null) return false; str = String(str); starts = String(starts); return str.length >= starts.length && str.slice(0, starts.length) === starts; }, endsWith: function(str, ends){ if (ends === '') return true; if (str == null || ends == null) return false; str = String(str); ends = String(ends); return str.length >= ends.length && str.slice(str.length - ends.length) === ends; }, splice: function(str, i, howmany, substr){ var arr = _s.chars(str); arr.splice(~~i, ~~howmany, substr); return arr.join(''); }, contains: function(str, needle){ if (needle === '') return true; if (str == null) return false; return String(str).indexOf(needle) !== -1; }, chars: function(str) { if (str == null) return []; return String(str).split(''); } }; module.exports = _s; },{}],5:[function(require,module,exports){ 'use strict'; var resolveUrl = require('resolve-url'); exports.resolve = function(from, to) { return resolveUrl(from, to); }; },{"resolve-url":54}],6:[function(require,module,exports){ 'use strict'; var minilog = require('minilog') , errorModule = require('./errors') , errors = errorModule.errors , createError = errorModule.createError , log = minilog('traverson'); exports.abortTraversal = function abortTraversal() { log.debug('aborting link traversal'); this.aborted = true; if (this.currentRequest) { log.debug('request in progress. trying to abort it, too.'); this.currentRequest.abort(); } }; exports.registerAbortListener = function registerAbortListener(t, callback) { if (t.currentRequest) { t.currentRequest.on('abort', function() { exports.callCallbackOnAbort(t); }); } }; exports.callCallbackOnAbort = function callCallbackOnAbort(t) { log.debug('link traversal aborted'); if (!t.callbackHasBeenCalledAfterAbort) { t.callbackHasBeenCalledAfterAbort = true; t.callback(exports.abortError(), t); } }; exports.abortError = function abortError() { var error = createError('Link traversal process has been aborted.', errors.TraversalAbortedError); error.aborted = true; return error; }; },{"./errors":9,"minilog":1}],7:[function(require,module,exports){ 'use strict'; var minilog = require('minilog') , log = minilog('traverson') , abortTraversal = require('./abort_traversal') , applyTransforms = require('./transforms/apply_transforms') , httpRequests = require('./http_requests') , isContinuation = require('./is_continuation') , walker = require('./walker'); var checkHttpStatus = require('./transforms/check_http_status') , continuationToDoc = require('./transforms/continuation_to_doc') , continuationToResponse = require('./transforms/continuation_to_response') , convertEmbeddedDocToResponse = require('./transforms/convert_embedded_doc_to_response') , extractDoc = require('./transforms/extract_doc') , extractResponse = require('./transforms/extract_response') , extractUrl = require('./transforms/extract_url') , fetchLastResource = require('./transforms/fetch_last_resource') , executeLastHttpRequest = require('./transforms/execute_last_http_request') , executeLastHttpRequestForConvertResponse = require('./transforms/execute_last_http_request_for_convert_response') , parse = require('./transforms/parse') , parseLinkHeader = require('./transforms/parse_link_header'); /** * Starts the link traversal process and end it with an HTTP get. */ exports.get = function(t, callback) { t.lastMethodName = 'GET'; var transformsAfterLastStep; if (t.convertResponseToObject) { transformsAfterLastStep = [ continuationToDoc, fetchLastResource, checkHttpStatus, parse, parseLinkHeader, extractDoc, ]; } else { transformsAfterLastStep = [ continuationToResponse, fetchLastResource, convertEmbeddedDocToResponse, extractResponse, ]; } walker.walk(t, transformsAfterLastStep, callback); return createTraversalHandle(t); }; /** * Special variant of get() that does not execute the last request but instead * yields the last URL to the callback. */ exports.getUrl = function(t, callback) { walker.walk(t, [ extractUrl ], callback); return createTraversalHandle(t); }; /** * Starts the link traversal process and sends an HTTP POST request with the * given body to the last URL. Passes the HTTP response of the POST request to * the callback. */ exports.post = function(t, callback) { t.lastMethodName = 'POST'; walkAndExecute(t, t.requestModuleInstance, t.requestModuleInstance.post, callback); return createTraversalHandle(t); }; /** * Starts the link traversal process and sends an HTTP PUT request with the * given body to the last URL. Passes the HTTP response of the PUT request to * the callback. */ exports.put = function(t, callback) { t.lastMethodName = 'PUT'; walkAndExecute(t, t.requestModuleInstance, t.requestModuleInstance.put, callback); return createTraversalHandle(t); }; /** * Starts the link traversal process and sends an HTTP PATCH request with the * given body to the last URL. Passes the HTTP response of the PATCH request to * the callback. */ exports.patch = function(t, callback) { t.lastMethodName = 'PATCH'; walkAndExecute(t, t.requestModuleInstance, t.requestModuleInstance.patch, callback); return createTraversalHandle(t); }; /** * Starts the link traversal process and sends an HTTP DELETE request to the * last URL. Passes the HTTP response of the DELETE request to the callback. */ exports.delete = function(t, callback) { t.lastMethodName = 'DELETE'; walkAndExecute(t, t.requestModuleInstance, t.requestModuleInstance.del, callback); return createTraversalHandle(t); }; function walkAndExecute(t, request, method, callback) { var transformsAfterLastStep; if (t.convertResponseToObject) { transformsAfterLastStep = [ executeLastHttpRequestForConvertResponse, checkHttpStatus, parse, parseLinkHeader, extractDoc, ]; } else { transformsAfterLastStep = [ executeLastHttpRequest, ]; } t.lastMethod = method; walker.walk(t, transformsAfterLastStep, callback); } function createTraversalHandle(t) { return { abort: t.abortTraversal }; } },{"./abort_traversal":6,"./http_requests":10,"./is_continuation":11,"./transforms/apply_transforms":18,"./transforms/check_http_status":19,"./transforms/continuation_to_doc":20,"./transforms/continuation_to_response":21,"./transforms/convert_embedded_doc_to_response":22,"./transforms/execute_last_http_request":24,"./transforms/execute_last_http_request_for_convert_response":25,"./transforms/extract_doc":26,"./transforms/extract_response":27,"./transforms/extract_url":28,"./transforms/fetch_last_resource":29,"./transforms/parse":32,"./transforms/parse_link_header":33,"./walker":39,"minilog":1}],8:[function(require,module,exports){ 'use strict'; var minilog = require('minilog') , standardRequest = require('request') , util = require('util'); var actions = require('./actions') , abortTraversal = require('./abort_traversal').abortTraversal , errorModule = require('./errors') , errors = errorModule.errors , createError = errorModule.createError , mediaTypeRegistry = require('./media_type_registry') , mediaTypes = require('./media_types') , mergeRecursive = require('./merge_recursive'); var log = minilog('traverson'); // Maintenance notice: The constructor is usually called without arguments, the // mediaType parameter is only used when cloning the request builder in // newRequest(). function Builder(mediaType, linkType) { this.mediaType = mediaType || mediaTypes.CONTENT_NEGOTIATION; this.linkType = linkType || 'link-rel'; this.adapter = this._createAdapter(this.mediaType); this.autoHeaders = true; this.contentNegotiation = true; this.rawPayloadFlag = false; this.convertResponseToObjectFlag = false; this.links = []; this.jsonParser = JSON.parse; this.requestModuleInstance = standardRequest; this.requestOptions = {}; this.resolveRelativeFlag = false; this.preferEmbedded = false; this.lastTraversalState = null; this.continuation = null; // Maintenance notice: when extending the list of configuration parameters, // also extend this.newRequest and initFromTraversalState } Builder.prototype._createAdapter = function(mediaType) { var AdapterType = mediaTypeRegistry.get(mediaType); if (!AdapterType) { throw createError('Unknown or unsupported media type: ' + mediaType, errors.UnsupportedMediaType); } log.debug('creating new ' + AdapterType.name); return new AdapterType(log); }; /** * Returns a new builder instance which is basically a clone of this builder * instance. This allows you to initiate a new request but keeping all the setup * (start URL, template parameters, request options, body parser, ...). */ Builder.prototype.newRequest = function() { var clonedRequestBuilder = new Builder(this.getMediaType(), this.getLinkType()); clonedRequestBuilder.useAutoHeaders(this.setsAutoHeaders()); clonedRequestBuilder.contentNegotiation = this.doesContentNegotiation(); clonedRequestBuilder.convertResponseToObject(this.convertsResponseToObject()); clonedRequestBuilder.from(shallowCloneArray(this.getFrom())); clonedRequestBuilder.withTemplateParameters( cloneArrayOrObject(this.getTemplateParameters())); clonedRequestBuilder.withRequestOptions( cloneArrayOrObject(this.getRequestOptions())); clonedRequestBuilder.withRequestLibrary(this.getRequestLibrary()); clonedRequestBuilder.parseResponseBodiesWith(this.getJsonParser()); clonedRequestBuilder.resolveRelative(this.doesResolveRelative()); clonedRequestBuilder.preferEmbeddedResources( this.doesPreferEmbeddedResources()); clonedRequestBuilder.continuation = this.continuation; // Maintenance notice: when extending the list of configuration parameters, // also extend initFromTraversalState return clonedRequestBuilder; }; /** * Disables content negotiation and forces the use of a given media type. * The media type has to be registered at Traverson's media type registry * before via traverson.registerMediaType (except for media type * application/json, which is traverson.mediaTypes.JSON). */ Builder.prototype.setMediaType = function(mediaType) { this.mediaType = mediaType || mediaTypes.CONTENT_NEGOTIATION; this.adapter = this._createAdapter(mediaType); this.contentNegotiation = (mediaType === mediaTypes.CONTENT_NEGOTIATION); return this; }; Builder.prototype.getLinkType = function() { return this.linkType; }; /** * Shortcut for * setMediaType(traverson.mediaTypes.JSON); */ Builder.prototype.json = function() { this.setMediaType(mediaTypes.JSON); return this; }; /** * Shortcut for * setMediaType(traverson.mediaTypes.JSON_HAL); */ Builder.prototype.jsonHal = function() { this.setMediaType(mediaTypes.JSON_HAL); return this; }; /** * Enables content negotiation (content negotiation is enabled by default, this * method can be used to enable it after a call to setMediaType disabled it). */ Builder.prototype.useContentNegotiation = function() { this.setMediaType(mediaTypes.CONTENT_NEGOTIATION); this.contentNegotiation = true; return this; }; /** * Set the root URL of the API, that is, where the link traversal begins. */ Builder.prototype.from = function(url) { this.startUrl = url; return this; }; /** * Adds link relations to the list of link relations to follow. The initial list * of link relations is the empty list. Each link relation in this list * corresponds to one step in the traversal. */ Builder.prototype.follow = function() { var newLinks = Array.prototype.slice.apply( arguments.length === 1 && util.isArray(arguments[0]) ? arguments[0] : arguments ); for (var i = 0; i < newLinks.length; i++) { if (typeof newLinks[i] === 'string') { newLinks[i] = { type: this.linkType, value: newLinks[i], }; } } this.links = this.links.concat(newLinks); return this; }; /** * Adds a special step to the list of link relations that will follow the * location header, that is, instead of reading the next URL from a link in the * document body, it uses the location header and follows the URL from this * header. */ Builder.prototype.followLocationHeader = function() { this.links.push({ type: 'header', value: 'location', }); return this; }; /** * Alias for follow. */ Builder.prototype.walk = Builder.prototype.follow; /** * Provide template parameters for URI template substitution. */ Builder.prototype.withTemplateParameters = function(parameters) { this.templateParameters = parameters; return this; }; /** * Provide options for HTTP requests (additional HTTP headers, for example). * This function resets any request options, that had been set previously, that * is, multiple calls to withRequestOptions are not cumulative. Use * addRequestOptions to add request options in a cumulative way. * * Options can either be passed as an object or an array. If an object is * passed, the options will be used for each HTTP request. If an array is * passed, each element should be an options object and the first array element * will be used for the first request, the second element for the second request * and so on. null elements are allowed. */ Builder.prototype.withRequestOptions = function(options) { this.requestOptions = options; return this; }; /** * Adds options for HTTP requests (additional HTTP headers, for example) on top * of existing options, if any. To reset all request options and set new ones * without keeping the old ones, you can use withRequestOptions. * * Options can either be passed as an object or an array. If an object is * passed, the options will be used for each HTTP request. If an array is * passed, each element should be an options object and the first array element * will be used for the first request, the second element for the second request * and so on. null elements are allowed. * * When called after a call to withRequestOptions or when combining multiple * addRequestOptions calls, some with objects and some with arrays, a multitude * of interesting situations can occur: * * 1) The existing request options are an object and the new options passed into * this method are also an object. Outcome: Both objects are merged and all * options are applied to all requests. * * 2) The existing options are an array and the new options passed into this * method are also an array. Outcome: Each array element is merged individually. * The combined options from the n-th array element in the existing options * array and the n-th array element in the given array are applied to the n-th * request. * * 3) The existing options are an object and the new options passed into this * method are an array. Outcome: A new options array will be created. For each * element, a clone of the existing options object will be merged with an * element from the given options array. * * Note that if the given array has less elements than the number of steps in * the link traversal (usually the number of steps is derived from the number * of link relations given to the follow method), only the first n http * requests will use options at all, where n is the number of elements in the * given array. HTTP request n + 1 and all following HTTP requests will use an * empty options object. This is due to the fact, that at the time of creating * the new options array, we can not know with certainty how many steps the * link traversal will have. * * 4) The existing options are an array and the new options passed into this * method are an object. Outcome: A clone of the given options object will be * merged into into each array element of the existing options. */ Builder.prototype.addRequestOptions = function(options) { // case 2: both the present options and the new options are arrays. // => merge each array element individually if (util.isArray(this.requestOptions) && util.isArray(options)) { mergeArrayElements(this.requestOptions, options); // case 3: there is an options object the new options are an array. // => create a new array, each element is a merge of the existing base object // and the array element from the new options array. } else if (typeof this.requestOptions === 'object' && util.isArray(options)) { this.requestOptions = mergeBaseObjectWithArrayElements(this.requestOptions, options); // case 4: there is an options array and the new options are an object. // => merge the new object into each array element. } else if (util.isArray(this.requestOptions) && typeof options === 'object') { mergeOptionObjectIntoEachArrayElement(this.requestOptions, options); // case 1: both are objects // => merge both objects } else { mergeRecursive(this.requestOptions, options); } return this; }; function mergeArrayElements(existingOptions, newOptions) { for (var i = 0; i < Math.max(existingOptions.length, newOptions.length); i++) { existingOptions[i] = mergeRecursive(existingOptions[i], newOptions[i]); } } function mergeBaseObjectWithArrayElements(existingOptions, newOptions) { var newOptArray = []; for (var i = 0; i < newOptions.length; i++) { newOptArray[i] = mergeRecursive(newOptions[i], existingOptions); } return newOptArray; } function mergeOptionObjectIntoEachArrayElement(existingOptions, newOptions) { for (var i = 0; i < existingOptions.length; i++) { mergeRecursive(existingOptions[i], newOptions); } } /** * Injects a custom request library. When using this method, you should not * call withRequestOptions or addRequestOptions but instead pre-configure the * injected request library instance before passing it to withRequestLibrary. */ Builder.prototype.withRequestLibrary = function(request) { this.requestModuleInstance = request; return this; }; /** * Injects a custom JSON parser. */ Builder.prototype.parseResponseBodiesWith = function(parser) { this.jsonParser = parser; return this; }; /** * Disables automatic Accept and Content-Type headers. See useAutoHeaders(). */ Builder.prototype.disableAutoHeaders = function() { return this.useAutoHeaders(false); }; /** * Enables automatic Accept and Content-Type headers. See useAutoHeaders(). */ Builder.prototype.enableAutoHeaders = function() { return this.useAutoHeaders(true); }; /** * Enables or disables automatic headers. With automatic headers enabled, * traverson will set default Accept and the Content-Type headers for HTTP * requests, unless you provide these headers explicitly with withRequestOptions * or addRequestOptions. * * The header values depend on the media type (see setMediaType()). For example, * for plain vanilla JSON (that is, when using setMediaType('application/json') * or the corresponding shortcut .json()), both headers will be sent with the * value 'application/json'. For HAL (that is, when using * setMediaType('application/hal+json') or the corresponding shortcut * jsonHal()), both headers will be sent with the value 'application/hal+json'. * * If the method is called without arguments (or the first argument is undefined * or null), automatic headers are turned on, otherwise the argument is * interpreted as a boolean flag. If it is a truthy value, auto headers * are enabled, if it is a falsy value (but not null or undefined), auto headers * are disabled. * * A note about the condition "unless you provide these headers explicitly with * withRequestOptions or addRequestOptions" in the first paragraph: Traverson * with automatic headers enabled will only check for the header option * "Accept" and "Content-Type", not for "accept" or "Content-type" or any other * variation regarding upper case/lower case letters. So to be on the safe * side, if you mix auto headers with explicitly specified headers, make sure * to specify your explicit headers with this exact same combination of upper * case and lower case letters. */ Builder.prototype.useAutoHeaders = function(flag) { if (typeof flag === 'undefined' || flag === null) { flag = true; } this.autoHeaders = !!flag; return this; }; /** * With this option enabled, the payload of the last request at the end of the * traversal will be sent as is, without stringifying it. The default is false, * which means that usually Traverson assumes the payload is passed as a * JavScript object which will then be stringified (which is the right thing to * do for JSON based MIME types like application/json. If you want to handle the * serialization yourself and don't want Traverson to interfere, this option * should be set to true. * * If the method is called without arguments (or the first argument is undefined * or null), this option is switched on, otherwise the argument is * interpreted as a boolean flag. If it is a truthy value, the option is * switched to on, if it is a falsy value (but not null or * undefined), the option is switched off. */ Builder.prototype.sendRawPayload = function(flag) { if (typeof flag === 'undefined' || flag === null) { flag = true; } this.rawPayloadFlag = !!flag; return this; }; /** * With this option enabled, the body of the response at the end of the * traversal will be converted into a JavaScript object (for example by passing * it into JSON.parse) and passing the resulting object into the callback. * The default is false, which means the full response is handed to the * callback. * * When response body conversion is enabled, you will not get the full * response, so you won't have access to the HTTP status code or headers. * Instead only the converted object will be passed into the callback. * * Note that the body of any intermediary responses during the traversal is * always converted by Traverson (to find the next link). * * If the method is called without arguments (or the first argument is undefined * or null), response body conversion is switched on, otherwise the argument is * interpreted as a boolean flag. If it is a truthy value, response body * conversion is switched to on, if it is a falsy value (but not null or * undefined), response body conversion is switched off. */ Builder.prototype.convertResponseToObject = function(flag) { if (typeof flag === 'undefined' || flag === null) { flag = true; } this.convertResponseToObjectFlag = !!flag; return this; }; /** * Switches URL resolution to relative (default is absolute) or back to * absolute. * * If the method is called without arguments (or the first argument is undefined * or null), URL resolution is switched to relative, otherwise the argument is * interpreted as a boolean flag. If it is a truthy value, URL resolution is * switched to relative, if it is a falsy value, URL resolution is switched to * absolute. */ Builder.prototype.resolveRelative = function(flag) { if (typeof flag === 'undefined' || flag === null) { flag = true; } this.resolveRelativeFlag = !!flag; return this; }; /** * Makes Traverson prefer embedded resources over traversing a link or vice * versa. This only applies to media types which support embedded resources * (like HAL). It has no effect when using a media type that does not support * embedded resources. * * It also only takes effect when a resource contains both a link _and_ an * embedded resource with the name that is to be followed at this step in the * link traversal process. * * If the method is called without arguments (or the first argument is undefined * or null), embedded resources will be preferred over fetching linked resources * with an additional HTTP request. Otherwise the argument is interpreted as a * boolean flag. If it is a truthy value, embedded resources will be preferred, * if it is a falsy value, traversing the link relation will be preferred. */ Builder.prototype.preferEmbeddedResources = function(flag) { if (typeof flag === 'undefined' || flag === null) { flag = true; } this.preferEmbedded = !!flag; return this; }; /** * Returns the current media type. If no media type is enforced but content type * detection is used, the string `content-negotiation` is returned. */ Builder.prototype.getMediaType = function() { return this.mediaType; }; /** * Returns the URL set by the from(url) method, that is, the root URL of the * API. */ Builder.prototype.getFrom = function() { return this.startUrl; }; /** * Returns the template parameters set by the withTemplateParameters. */ Builder.prototype.getTemplateParameters = function() { return this.templateParameters; }; /** * Returns the request options set by the withRequestOptions or * addRequestOptions. */ Builder.prototype.getRequestOptions = function() { return this.requestOptions; }; /** * Returns the custom request library instance set by withRequestLibrary or the * standard request library instance, if a custom one has not been set. */ Builder.prototype.getRequestLibrary = function() { return this.requestModuleInstance; }; /** * Returns the custom JSON parser function set by parseResponseBodiesWith or the * standard parser function, if a custom one has not been set. */ Builder.prototype.getJsonParser = function() { return this.jsonParser; }; /** * Returns true if default Accept and the Content-Type headers will be set * automatically for HTTP requests. */ Builder.prototype.setsAutoHeaders = function() { return this.autoHeaders; }; /** * Returns true if the given payload will be sent without stringifying it first. */ Builder.prototype.sendsRawPayload = function() { return this.rawPayloadFlag; }; /** * Returns true if the body of the last response will be converted to a * JavaScript object before passing the result back to the callback. */ Builder.prototype.convertsResponseToObject = function() { return this.convertResponseToObjectFlag; }; /** * Returns the flag controlling if URLs are resolved relative or absolute. * A return value of true means that URLs are resolved relative, false means * absolute. */ Builder.prototype.doesResolveRelative = function() { return this.resolveRelativeFlag; }; /** * Returns the flag controlling if embedded resources are preferred over links. * A return value of true means that embedded resources are preferred, false * means that following links is preferred. */ Builder.prototype.doesPreferEmbeddedResources = function() { return this.preferEmbedded; }; /** * Returns true if content negotiation is enabled and false if a particular * media type is forced. */ Builder.prototype.doesContentNegotiation = function() { return this.contentNegotiation; }; /** * Starts the link traversal process and passes the last HTTP response to the * callback. */ Builder.prototype.get = function get(callback) { log.debug('initiating traversal (get)'); var t = createInitialTraversalState(this); return actions.get(t, wrapForContinue(this, t, callback, 'get')); }; /** * Special variant of get() that does not yield the full http response to the * callback but instead the already parsed JSON as an object. * * This is a shortcut for builder.convertResponseToObject().get(callback). */ Builder.prototype.getResource = function getResource(callback) { log.debug('initiating traversal (getResource)'); this.convertResponseToObjectFlag = true; var t = createInitialTraversalState(this); return actions.get(t, wrapForContinue(this, t, callback, 'getResource')); }; /** * Special variant of get() that does not execute the last request but instead * yields the last URL to the callback. */ Builder.prototype.getUrl = function getUrl(callback) { log.debug('initiating traversal (getUrl)'); var t = createInitialTraversalState(this); return actions.getUrl(t, wrapForContinue(this, t, callback, 'getUrl')); }; /** * Alias for getUrl. */ Builder.prototype.getUri = Builder.prototype.getUrl; /** * Starts the link traversal process and sends an HTTP POST request with the * given body to the last URL. Passes the HTTP response of the POST request to * the callback. */ Builder.prototype.post = function post(body, callback) { log.debug('initiating traversal (post)'); var t = createInitialTraversalState(this, body); return actions.post(t, wrapForContinue(this, t, callback, 'post')); }; /** * Starts the link traversal process and sends an HTTP PUT request with the * given body to the last URL. Passes the HTTP response of the PUT request to * the callback. */ Builder.prototype.put = function put(body, callback) { log.debug('initiating traversal (put)'); var t = createInitialTraversalState(this, body); return actions.put(t, wrapForContinue(this, t, callback, 'put')); }; /** * Starts the link traversal process and sends an HTTP PATCH request with the * given body to the last URL. Passes the HTTP response of the PATCH request to * the callback. */ Builder.prototype.patch = function patch(body, callback) { log.debug('initiating traversal (patch)'); var t = createInitialTraversalState(this, body); return actions.patch(t, wrapForContinue(this, t, callback, 'patch')); }; /** * Starts the link traversal process and sends an HTTP DELETE request to the * last URL. Passes the HTTP response of the DELETE request to the callback. */ Builder.prototype.delete = function del(callback) { log.debug('initiating traversal (delete)'); var t = createInitialTraversalState(this); return actions.delete(t, wrapForContinue(this, t, callback, 'delete')); }; /** * Alias for delete. */ Builder.prototype.del = Builder.prototype.delete; /** * Set linkType property indicating that traverson must follow relations from * header 'link' */ Builder.prototype.linkHeader = function() { this.linkType = 'link-header'; return this; }; function createInitialTraversalState(self, body) { var normalizedBody = (body !== null && typeof body !== undefined) ? body : null; var traversalState = { aborted: false, adapter: self.adapter || null, body: normalizedBody, callbackHasBeenCalledAfterAbort: false, autoHeaders: self.setsAutoHeaders(), contentNegotiation: self.doesContentNegotiation(), continuation: null, rawPayload: self.sendsRawPayload(), convertResponseToObject: self.convertsResponseToObject(), links: self.links, jsonParser: self.getJsonParser(), requestModuleInstance: self.getRequestLibrary(), requestOptions: self.getRequestOptions(), resolveRelative: self.doesResolveRelative(), preferEmbedded: self.doesPreferEmbeddedResources(), startUrl: self.startUrl, step : { url: self.startUrl, index: 0, }, templateParameters: self.getTemplateParameters(), }; traversalState.abortTraversal = abortTraversal.bind(traversalState); if (self.continuation) { traversalState.continuation = self.continuation; traversalState.step = self.continuation.step; self.continuation = null; } return traversalState; } function wrapForContinue(self, t, callback, firstTraversalAction) { return function(err, result) { if (err) { return callback(err); } return callback(null, result, { continue: function() { if (!t) { throw createError('No traversal state to continue from.', errors.InvalidStateError); } log.debug('> continuing finished traversal process'); self.continuation = { step: t.step, action: firstTraversalAction, }; self.continuation.step.index = 0; initFromTraversalState(self, t); return self; }, }); }; } /* * Copy configuration from traversal state to builder instance to * prepare for next traversal process. */ function initFromTraversalState(self, t) { self.aborted = false; self.adapter = t.adapter; self.body = t.body; self.callbackHasBeenCalledAfterAbort = false; self.autoHeaders = t.autoHeaders; self.contentNegotiation = t.contentNegotiation; self.rawPayload = t.rawPayload; self.convertResponseToObjectFlag = t.convertResponseToObject; self.links = []; self.jsonParser = t.jsonParser; self.requestModuleInstance = t.requestModuleInstance, self.requestOptions = t.requestOptions, self.resolveRelativeFlag = t.resolveRelative; self.preferEmbedded = t.preferEmbedded; self.startUrl = t.startUrl; self.templateParameters = t.templateParameters; } function cloneArrayOrObject(thing) { if (util.isArray(thing)) { return shallowCloneArray(thing); } else if (typeof thing === 'object') { return deepCloneObject(thing); } else { return thing; } } function deepCloneObject(object) { return mergeRecursive(null, object); } function shallowCloneArray(array) { if (!array) { return array; } return array.slice(0); } module.exports = Builder; },{"./abort_traversal":6,"./actions":7,"./errors":9,"./media_type_registry":13,"./media_types":14,"./merge_recursive":15,"minilog":1,"request":3,"util":2}],9:[function(require,module,exports){ 'use strict'; module.exports = { errors: { HTTPError: 'HTTPError', InvalidArgumentError: 'InvalidArgumentError', InvalidStateError: 'InvalidStateError', JSONError: 'JSONError', JSONPathError: 'JSONPathError', LinkError: 'LinkError', TraversalAbortedError: 'TraversalAbortedError', UnsupportedMediaType: 'UnsupportedMediaTypeError', }, createError: function(message, name, data) { var error = new Error(message); error.name = name; if (data) { error.data = data; } return error; }, }; },{}],10:[function(require,module,exports){ (function (process){(function (){ 'use strict'; var minilog = require('minilog') , log = minilog('traverson') , abortTraversal = require('./abort_traversal') , detectContentType = require('./transforms/detect_content_type') , errorModule = require('./errors') , errors = errorModule.errors , createError = errorModule.createError , getOptionsForStep = require('./transforms/get_options_for_step'); var nextTickAvailable = process && Object.hasOwnProperty.call(process, 'nextTick'); /** * Executes a HTTP GET request during the link traversal process. */ // This method is currently used for all intermediate GET requests during the // link traversal process. Coincidentally, it is also used for the final request // in a link traversal should this happen to be a GET request. Otherwise (POST/ // PUT/PATCH/DELETE), Traverson uses exectueHttpRequest. exports.fetchResource = function fetchResource(t, callback) { log.debug('fetching resource for next step'); if (t.step.url) { log.debug('fetching resource from', t.step.url); return executeHttpGet(t, callback); } else if (t.step.doc) { // The step already has an attached result document, so all is fine and we // can call the callback immediately log.debug('resource for next step has already been fetched, using ' + 'embedded'); if (nextTickAvailable) { return process.nextTick(function() { callback(null, t); }); } return callback(null, t); } else { var error = createError('Can not process step.', errors.InvalidStateError); error.step = t.step; if (nextTickAvailable) { return process.nextTick(function() { callback(error, t); }); } return callback(error, t); } }; function executeHttpGet(t, callback) { var options = getOptionsForStep(t); log.debug('HTTP GET request to', t.step.url); log.debug('options', options); t.mostRecentHttpMethodName = 'GET'; t.currentRequest = t.requestModuleInstance.get(t.step.url, options, function(err, response, body) { log.debug('HTTP GET request to', t.step.url, 'returned'); t.currentRequest = null; // workaround for cases where response body is empty but body comes in as // the third argument if (body && !response.body) { response.body = body; } t.step.response = response; if (err) { return callback(err, t); } log.debug('request to', t.step.url, 'finished without error (', response.statusCode, ')'); if (!detectContentType(t, callback)) return; return callback(null, t); }); abortTraversal.registerAbortListener(t, callback); } /** * Executes an arbitrary HTTP request. */ // This method is currently used for POST/PUT/PATCH/DELETE at the end of a link // traversal process. If the link traversal process requires a GET as the last // request, Traverson uses exectueHttpGet. exports.executeHttpRequest = function(t, request, method, methodName, callback) { var requestOptions = getOptionsForStep(t); if (t.body !== null && typeof t.body !== 'undefined') { requestOptions.body = (t.rawPayload || requestOptions.jsonReplacer) ? t.body : JSON.stringify(t.body); } log.debug('HTTP', methodName, 'request to', t.step.url); log.debug('options', requestOptions); t.mostRecentHttpMethodName = methodName; t.currentRequest = method.call(request, t.step.url, requestOptions, function(err, response, body) { log.debug('HTTP', methodName, 'request to', t.step.url, 'returned'); t.currentRequest = null; // workaround for cases where response body is empty but body comes in as // the third argument if (body && !response.body) { response.body = body; } t.step.response = response; if (err) { return callback(err); } return callback(null, response); }); abortTraversal.registerAbortListener(t, callback); }; }).call(this)}).call(this,require('_process')) },{"./abort_traversal":6,"./errors":9,"./transforms/detect_content_type":23,"./transforms/get_options_for_step":31,"_process":53,"minilog":1}],11:[function(require,module,exports){ 'use strict'; module.exports = function isContinuation(t) { return t.continuation && t.step && t.step.response; }; },{}],12:[function(require,module,exports){ 'use strict'; var minilog = require('minilog') , _s = require('underscore.string'); var jsonpath; try { jsonpath = require('jsonpath-plus'); } catch (e) { jsonpath = false; console.warn('Could not require jsonpath-plus, JSONPath support is not ' + 'available.'); } var errorModule = require('./errors') , errors = errorModule.errors , createError = errorModule.createError , parseLinkHeaderValue = require('./parse_link_header_value'); function JsonAdapter(log) { this.log = log; } JsonAdapter.mediaType = 'application/json'; JsonAdapter.prototype.findNextStep = function(t, link) { validateLinkObject(link); var doc = t.lastStep.doc; this.log.debug('resolving link', link); switch (link.type) { case 'link-rel': return this._handleLinkRel(doc, link); case 'header': return this._handleHeader(t.lastStep.response, link); case 'link-header': return this._handleLinkHeader(t.lastStep.response, link); default: throw createError('Link objects with type ' + link.type + ' are not ' + 'supported by this adapter.', errors.InvalidArgumentError, link); } }; JsonAdapter.prototype._handleLinkRel = function(doc, link) { var linkRel = link.value; this.log.debug('looking for link-rel', linkRel, 'in doc', doc); var url; if (this._testJSONPath(linkRel)) { return { url: this._resolveJSONPath(doc, linkRel) }; } else if (doc[linkRel]) { return { url : doc[linkRel] }; } else { throw createError('Could not find property ' + linkRel + ' in document.', errors.LinkError, doc); } }; function validateLinkObject(link) { if (typeof link === 'undefined' || link === null) { throw createError('Link object is null or undefined.', errors.InvalidArgumentError); } if (typeof link !== 'object') { throw createError('Links must be objects, not ' + typeof link + '.', errors.InvalidArgumentError, link); } if (!link.type) { throw createError('Link objects has no type attribute.', errors.InvalidArgumentError, link); } } JsonAdapter.prototype._testJSONPath = function(link) { return _s.startsWith(link, '$.') || _s.startsWith(link, '$['); }; JsonAdapter.prototype._resolveJSONPath = function(doc, link) { if (!jsonpath) { throw createError('JSONPath support is not available.'); } var matches = jsonpath({ json: doc, path: link, }); if (matches.length === 1) { var url = matches[0]; if (!url) { throw createError('JSONPath expression ' + link + ' was resolved but the result was null, undefined or an empty' + ' string in document:\n' + JSON.stringify(doc), errors.JSONPathError, doc); } if (typeof url !== 'string') { throw createError('JSONPath expression ' + link + ' was resolved but the result is not a property of type string. ' + 'Instead it has type "' + (typeof url) + '" in document:\n' + JSON.stringify(doc), errors.JSONPathError, doc); } return url; } else if (matches.length > 1) { // ambigious match throw createError('JSONPath expression ' + link + ' returned more than one match in document:\n' + JSON.stringify(doc), errors.JSONPathError, doc); } else { // no match at all throw createError('JSONPath expression ' + link + ' returned no match in document:\n' + JSON.stringify(doc), errors.JSONPathError, doc); } }; JsonAdapter.prototype._handleHeader = function(httpResponse, link) { switch (link.value) { case 'location': var locationHeader = httpResponse.headers.location; if (!locationHeader) { throw createError('Following the location header but there was no ' + 'location header in the last response.', errors.LinkError, httpResponse.headers); } return { url : locationHeader }; default: throw createError('Link objects with type header and value ' + link.value + ' are not supported by this adapter.', errors.InvalidArgumentError, link); } }; JsonAdapter.prototype._handleLinkHeader = function(httpResponse, link) { if (!httpResponse.headers.link) throw createError('There was no link header in the last response.', errors.InvalidArgumentError, link); var links = parseLinkHeaderValue(httpResponse.headers.link); if (links[link.value]) { return { url : links[link.value].url}; } else { throw createError('Link with relation ' + link.value + ' not found in link header.', errors.InvalidArgumentError, link); } }; module.exports = JsonAdapter; },{"./errors":9,"./parse_link_header_value":17,"jsonpath-plus":51,"minilog":1,"underscore.string":4}],13:[function(require,module,exports){ 'use strict'; var mediaTypes = require('./media_types'); var registry = {}; exports.register = function register(contentType, constructor) { registry[contentType] = constructor; }; exports.get = function get(contentType) { return registry[contentType]; }; exports.register(mediaTypes.CONTENT_NEGOTIATION, require('./negotiation_adapter')); exports.register(mediaTypes.JSON, require('./json_adapter')); },{"./json_adapter":12,"./media_types":14,"./negotiation_adapter":16}],14:[function(require,module,exports){ 'use strict'; var JsonAdapter = require('./json_adapter'); module.exports = { CONTENT_NEGOTIATION: 'content-negotiation', JSON: JsonAdapter.mediaType, JSON_HAL: 'application/hal+json', }; },{"./json_adapter":12}],15:[function(require,module,exports){ 'use strict'; // TODO Maybe replace with https://github.com/Raynos/xtend // check browser build size, though. function mergeRecursive(obj1, obj2) { if (!obj1 && obj2) { obj1 = {}; } for (var key in obj2) { if (!obj2.hasOwnProperty(key)) { continue; } merge(obj1, obj2, key); } return obj1; } function merge(obj1, obj2, key) { if (typeof obj2[key] === 'object') { // if it is an object (that is, a non-leave in the tree), // and it is not present in obj1 if (!obj1[key] || typeof obj1[key] !== 'object') { // ... we create an empty object in obj1 obj1[key]