UNPKG

@sap_oss/odata-library

Version:

OData client for testing Netweawer OData services.

351 lines (324 loc) 9.1 kB
"use strict"; const EventEmitter = require("events"); const _ = require("lodash"); const HTTPParser = require("http-parser-js").HTTPParser; const parsers = require("../parsers"); const Headers = require("./Headers"); const responseType = require("../../engine/responseType"); /** * Response class implements OData particular response * * @public * @class Response */ class Response extends EventEmitter { /** * Initialize instance of the batch Response class * * @param {Array} rawResponse particular response content from whole batch mulitpart/mime response (lines are array items) * * @public * @memberof Response */ constructor(rawResponse) { let resolveResponse; let rejectResponse; super(); Object.defineProperty(this, "promise", { value: new Promise(function (resolve, reject) { resolveResponse = resolve; rejectResponse = reject; }), writable: false, }); Object.defineProperty(this, "resolve", { value: resolveResponse, writable: false, }); Object.defineProperty(this, "reject", { value: rejectResponse, writable: false, }); this.process(rawResponse); } /** * Parse and resolve/reject Response.promise * * @param {Array} rawResponse particular response content from whole batch mulitpart/mime response (lines are array items) * * @returns {Object} initialized parser (for testing usage) * * @private * @memberof Response */ process(rawResponse) { this.body = null; _.each( this.parseDivideResponse(rawResponse), (responseRawValue, responseRawKey) => { this[responseRawKey] = responseRawValue; } ); let parser = new HTTPParser(HTTPParser.RESPONSE); let error; try { parser.onHeadersComplete = this.handlerHeadersComplete.bind(this); parser.onBody = this.handlerBody.bind(this); parser.onMessageComplete = this.handlerMessageComplete.bind(this); parser.execute( Buffer.from( _.chain(this.rawHTTPResponse) .filter((header) => !header.match(/content-length/i)) .join("\n") .value(), "binary" ) ); parser.finish(); if (parser.state === "HEADER") { this.processHeaderInfo(parser.info); this.finishProcessResponse(parser.info.statusCode); } } catch (ex) { error = new Error("Unexpected error thrown for response parsing."); error.response = this; this.reject(error); } return parser; } /** * Divide rawResponse to part for MIME content and part of HTTP content * * @param {Array} rawResponse particular response content from whole batch mulitpart/mime response (lines are array items) * * @returns {Object} object with "rawHTTPResponse" key and "rawMIMEHeaders" key * * @private * @memberof Response */ parseDivideResponse(rawResponse) { let blocks = _.reduce( rawResponse, (acc, row) => { if (row === "") { acc.push([]); } else { acc[acc.length - 1].push(row); } return acc; }, [[]] ); return { rawHTTPResponse: _.concat(blocks[1], "", blocks[2]), rawMIMEHeaders: blocks[0], }; } /** * Just for compatibility with NodeJS HTTP.Response object * * @private * @memberof Response */ setEncoding() {} /** * Divide rawResponse to part for MIME content and part of HTTP content * * @returns {String} content type of the particular batch HTTP response * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type * * @private * @memberof Response */ getContentType() { let mediaTypeEnd; let contentType = _.find( this.headers, (headerValue, headerName) => _.isString(headerName) && headerName.toLowerCase() === "content-type" ); if (_.isString(contentType)) { mediaTypeEnd = contentType.indexOf(";"); contentType = contentType.substring( 0, mediaTypeEnd < 0 ? contentType.length : mediaTypeEnd ); } else { contentType = null; } return contentType; } /** * Determine parser for the current response * * @returns {Function} function which is compatible with superagent parser * * @see http://visionmedia.github.io/superagent/#parsing-response-bodies * * @private * @memberof Response */ getBodyParser() { return parsers[this.getContentType()]; } /** * Handler which is called after the body parsing is done * * @param {Error} err is error object raised during body parsing * @param {Any} content parsed content * * @private * @memberof Response */ handlerParserFinished(err, content) { if (err) { this.resolve(err); } else { this.body = content; this.finishProcessResponse( this.statusCode, JSON.stringify(content, null, 2) ); } } /** * Helper function which sets correct parser after the headers are * loaded and parsed * * Use by handlerHeadersComplete method * * @param {Function} bodyParser is function function which is compatible with superagent parser * * @see http://visionmedia.github.io/superagent/#parsing-response-bodies * * @private * @memberof Response */ useBodyParser(bodyParser) { if (bodyParser) { bodyParser(this, this.handlerParserFinished.bind(this)); } else { this.on("end", this.handlerParserFinished.bind(this)); } } /** * Handler called when headers are received. Parse headers and set correct body * parser. * * @param {Object} headersInfo object with "headers" key which contains array * of headers. The headers are set as rawHeaders to the Response object * parsed headers are accessible as headers object * * @see http://visionmedia.github.io/superagent/#parsing-response-bodies * * @private * @memberof Response */ handlerHeadersComplete(headersInfo) { this.processHeaderInfo(headersInfo); this.useBodyParser(this.getBodyParser()); } /** * Append parsed headers to the batch response instance * * @param {Object} headersInfo object with "headers" key which contains array * of headers. The headers are set as rawHeaders to the Response object * parsed headers are accessible as headers object * * @see http://visionmedia.github.io/superagent/#parsing-response-bodies * * @private * @memberof Response */ processHeaderInfo(headersInfo) { _.each(headersInfo, (headerInfoValue, headerInfoKey) => { this[headerInfoKey] = headerInfoValue; }); this.rawHeaders = this.headers; this.headers = new Headers(this.rawHeaders); } /** * Fire event "data" after the new data are recevied * * @param {Buffer} data buffet with body of the HTTP response * @param {Number} offset offset of currently received data * @param {Number} len length of currently received data * * @private * @memberof Response */ handlerBody(data, offset, len) { this.emit("data", data.toString("binary", offset, offset + len)); } /** * Fire event "end" when response is fully received and parsed * * @private * @memberof Response */ handlerMessageComplete() { this.emit("end"); } /** * Finish response processing * * @param {Number} statusCode HTTP status code from raw response * @param {String} errorMessage response * * @private * @memberof Response */ finishProcessResponse(statusCode, errorMessage) { let message; let error; if (statusCode < 400) { this.resolve(this); } else { message = `${statusCode} - Invalid response inside Batch.`; if (_.isString(errorMessage)) { message += `\n${errorMessage}`; } error = new Error(message); error.response = this; this.resolve(error); } } /** * Read plain OData response as javascript object from * Batch response * * @public * * @param {String} listResultPath path to list result (depends on OData version) * @param {String} instanceResultPath path to entity result (depends on OData version) * * @returns {Array|Object} parsed list or entity * * @memberof agent/batch/Response */ plain(listResultPath, instanceResultPath) { let result; switch (this.request.responseType) { case responseType.COUNT: result = parsers.count(_.get(this, "body")); break; case responseType.LIST: result = _.get(this, `body.${listResultPath}`); break; case responseType.ENTITY: result = _.get(this, `body.${instanceResultPath}`, null); break; } return result || this; } /** * It is mimicry for Fetch API Response json method * * @returns {Promise} promise resolved by body as json format */ json() { return Promise.resolve(this.body); } } module.exports = Response;