traverson
Version:
Hypermedia API/HATEOAS client for Node.js and the browser
1,551 lines (1,373 loc) • 250 kB
JavaScript
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]