1 | // @flow
|
2 |
|
3 | import type { Subrequest } from '../types/BlueprintManager';
|
4 | import type { Response } from '../types/Responses';
|
5 | import type { RequestorInterface } from '../types/Requestor';
|
6 |
|
7 | const request = require('request');
|
8 | const 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 | */
|
21 | module.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 | };
|