UNPKG

5.46 kBJavaScriptView Raw
1// @flow
2
3import type { Subrequest } from '../types/BlueprintManager';
4import type { Response } from '../types/Responses';
5import type { RequestorInterface } from '../types/Requestor';
6
7const request = require('request');
8const querystring = require('querystring');
9
10/**
11 * @classdesc
12 * Make subrequests via HTTP.
13 *
14 * This is the most basic requestor, typically you will want to override this to
15 * make sure that you can optimize for your stack. Examples of these are:
16 * cache-wrapped subrequests, resolve requests locally when the request is for
17 * the current server, multiplex unix socket connections with HTTP ones, …
18 *
19 * @class HttpRequestor
20 */
21module.exports = class HttpRequestor implements RequestorInterface {
22 /**
23 * Makes multiple requests in parallel.
24 *
25 * @param {Subrequest[]} subrequests
26 * The list of requests to make.
27 *
28 * @return {Promise.<Response[]>}
29 * The responses to the requests.
30 */
31 requestInParallel(subrequests: Array<Subrequest>): Array<Promise<Response>> {
32 return subrequests.map((subrequest) => {
33 // Build the request options.
34 const { uri, qs } = this._parseQueryString(subrequest.uri);
35 const options: Object = {
36 // Add a custom request header to identify the subrequest.
37 headers: { 'x-subrequest-id': subrequest.requestId },
38 qs,
39 };
40 subrequest.headers.forEach((value, name) => {
41 options.headers[name] = value;
42 });
43 // Set the `json` option if applicable.
44 const contentType = subrequest.headers.get('Content-Type')
45 || subrequest.headers.get('content-type') || '';
46 options.json = !!contentType.match(new RegExp('[/\+]json$')); // eslint-disable-line no-useless-escape
47 if (subrequest.body) {
48 options.body = subrequest.body;
49 }
50
51 const method = this.constructor._actionToMethod(subrequest.action).toLowerCase();
52 return this._individualRequest(method, uri, options, subrequest.requestId);
53 });
54 }
55
56 /**
57 * Parses the query string and returns the uri and query object.
58 *
59 * @param {string} uri
60 * The URI with query string encoded.
61 *
62 * @return {{uri: string, qs: *}}
63 * The query string and URI.
64 *
65 * @protected
66 */
67 _parseQueryString(uri: string) {
68 const parts = uri.split('?');
69 const qs = querystring.parse(parts[1] || '');
70 return { uri: parts[0], qs };
71 }
72
73 /**
74 * Execute an individual request.
75 *
76 * @param {string} method
77 * The HTTP method.
78 * @param {string} uri
79 * The URI to make the request against.
80 * @param {Object} options
81 * A list of options for the requesting library.
82 * @param {string} subrequestId
83 * The ID of the request being made.
84 *
85 * @returns {Promise<Response>}
86 * A response promise.
87 *
88 * @private
89 */
90 _individualRequest(
91 method: string,
92 uri: string,
93 options: Object,
94 subrequestId: string
95 ): Promise<Response> {
96 return new Promise((resolve, reject) => {
97 request[method](uri, options, (error, response) => {
98 if (error) {
99 reject(error);
100 }
101 else {
102 this._setSubrequestResponseHeader(response, subrequestId);
103 resolve(response);
104 }
105 });
106 });
107 }
108
109 /**
110 * Translate the response from the lib format into the typed Response object.
111 *
112 * Responses come back as an object defined in the request module. We want to
113 * transform them into the typed object.
114 *
115 * @param {*} response
116 * The response in the library object.
117 *
118 * @return {Response}
119 * The response in the common structure.
120 */
121 translateResponseFromLibFormat(response: *): Response {
122 // Translate the headers from an object to a Map.
123 const headers = new Map(Object.keys(response.headers)
124 .map(name => [name, response.headers[name]]));
125 return { headers, body: response.body };
126 }
127
128 /**
129 * Set the subrequest ID in the response object.
130 *
131 * This is used later on for ID purposes when replacing data based on this
132 * response down the road.
133 *
134 * @param {*} response
135 * The response in the library object.
136 * @param {string} id
137 * The request ID in memory.
138 *
139 * @return {void}
140 *
141 * @private
142 */
143 _setSubrequestResponseHeader(response: *, id: string): void { // eslint-disable-line no-unused-vars
144 // In this case we prefer setting the id from the request object inside the
145 // response, for consistency.
146 const subrequestId: string = response.req._headers['x-subrequest-id'];
147 response.headers['x-subrequest-id'] = subrequestId;
148 response.rawHeaders.push('x-subrequest-id');
149 response.rawHeaders.push(subrequestId);
150 }
151
152 /**
153 * Translate Subrequests specification actions into HTTP methods.
154 *
155 * @param {string} action
156 * The action
157 *
158 * @return {string}
159 * The HTTP method. Capitalized.
160 *
161 * @private
162 */
163 static _actionToMethod(action: string): string {
164 // For good old HTTP the actions cleanly map to HTTP methods.
165 switch (action) {
166 case 'view':
167 return 'GET';
168 case 'create':
169 return 'POST';
170 case 'update':
171 return 'PATCH';
172 case 'replace':
173 return 'PUT';
174 case 'delete':
175 return 'DELETE';
176 case 'exists':
177 return 'HEAD';
178 case 'discover':
179 return 'OPTIONS';
180 default:
181 throw new Error('Unexpected action. Impossible to map to an HTTP method.');
182 }
183 }
184};