UNPKG

14.2 kBJavaScriptView Raw
1"use strict";
2// Copyright 2020 Google LLC
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14Object.defineProperty(exports, "__esModule", { value: true });
15exports.createAPIRequest = void 0;
16const google_auth_library_1 = require("google-auth-library");
17const qs = require("qs");
18const stream = require("stream");
19const urlTemplate = require("url-template");
20const uuid = require("uuid");
21const extend = require("extend");
22const isbrowser_1 = require("./isbrowser");
23const h2 = require("./http2");
24// eslint-disable-next-line @typescript-eslint/no-var-requires
25const pkg = require('../../package.json');
26// eslint-disable-next-line @typescript-eslint/no-explicit-any
27function isReadableStream(obj) {
28 return (obj !== null &&
29 typeof obj === 'object' &&
30 typeof obj.pipe === 'function' &&
31 obj.readable !== false &&
32 typeof obj._read === 'function' &&
33 typeof obj._readableState === 'object');
34}
35function getMissingParams(params, required) {
36 const missing = new Array();
37 required.forEach(param => {
38 // Is the required param in the params object?
39 if (params[param] === undefined) {
40 missing.push(param);
41 }
42 });
43 // If there are any required params missing, return their names in array,
44 // otherwise return null
45 return missing.length > 0 ? missing : null;
46}
47function createAPIRequest(parameters, callback) {
48 if (callback) {
49 createAPIRequestAsync(parameters).then(r => callback(null, r), callback);
50 }
51 else {
52 return createAPIRequestAsync(parameters);
53 }
54}
55exports.createAPIRequest = createAPIRequest;
56async function createAPIRequestAsync(parameters) {
57 var _a, _b, _c, _d;
58 // Combine the GaxiosOptions options passed with this specific
59 // API call with the global options configured at the API Context
60 // level, or at the global level.
61 const options = extend(true, {}, // Ensure we don't leak settings upstream
62 ((_a = parameters.context.google) === null || _a === void 0 ? void 0 : _a._options) || {}, // Google level options
63 parameters.context._options || {}, // Per-API options
64 parameters.options // API call params
65 );
66 const params = extend(true, {}, // New base object
67 options.params, // Combined global/per-api params
68 parameters.params // API call params
69 );
70 options.userAgentDirectives = options.userAgentDirectives || [];
71 const media = params.media || {};
72 /**
73 * In a previous version of this API, the request body was stuffed in a field
74 * named `resource`. This caused lots of problems, because it's not uncommon
75 * to have an actual named parameter required which is also named `resource`.
76 * This meant that users would have to use `resource_` in those cases, which
77 * pretty much nobody figures out on their own. The request body is now
78 * documented as being in the `requestBody` property, but we also need to keep
79 * using `resource` for reasons of back-compat. Cases that need to be covered
80 * here:
81 * - user provides just a `resource` with a request body
82 * - user provides both a `resource` and a `resource_`
83 * - user provides just a `requestBody`
84 * - user provides both a `requestBody` and a `resource`
85 */
86 let resource = params.requestBody;
87 if (!params.requestBody &&
88 params.resource &&
89 (!parameters.requiredParams.includes('resource') ||
90 typeof params.resource !== 'string')) {
91 resource = params.resource;
92 delete params.resource;
93 }
94 delete params.requestBody;
95 let authClient = params.auth || options.auth;
96 const defaultMime = typeof media.body === 'string' ? 'text/plain' : 'application/octet-stream';
97 delete params.media;
98 delete params.auth;
99 // Grab headers from user provided options
100 const headers = params.headers || {};
101 populateAPIHeader(headers);
102 delete params.headers;
103 // Un-alias parameters that were modified due to conflicts with reserved names
104 Object.keys(params).forEach(key => {
105 if (key.slice(-1) === '_') {
106 const newKey = key.slice(0, -1);
107 params[newKey] = params[key];
108 delete params[key];
109 }
110 });
111 // Check for missing required parameters in the API request
112 const missingParams = getMissingParams(params, parameters.requiredParams);
113 if (missingParams) {
114 // Some params are missing - stop further operations and inform the
115 // developer which required params are not included in the request
116 throw new Error('Missing required parameters: ' + missingParams.join(', '));
117 }
118 // Parse urls
119 if (options.url) {
120 let url = options.url;
121 if (typeof url === 'object') {
122 url = url.toString();
123 }
124 options.url = urlTemplate.parse(url).expand(params);
125 }
126 if (parameters.mediaUrl) {
127 parameters.mediaUrl = urlTemplate.parse(parameters.mediaUrl).expand(params);
128 }
129 // Rewrite url if rootUrl is globally set
130 if (parameters.context._options.rootUrl !== undefined &&
131 options.url !== undefined) {
132 const originalUrl = new URL(options.url);
133 const path = originalUrl.href.substr(originalUrl.origin.length);
134 options.url = new URL(path, parameters.context._options.rootUrl).href;
135 }
136 // When forming the querystring, override the serializer so that array
137 // values are serialized like this:
138 // myParams: ['one', 'two'] ---> 'myParams=one&myParams=two'
139 // This serializer also encodes spaces in the querystring as `%20`,
140 // whereas the default serializer in gaxios encodes to a `+`.
141 options.paramsSerializer = params => {
142 return qs.stringify(params, { arrayFormat: 'repeat' });
143 };
144 // delete path params from the params object so they do not end up in query
145 parameters.pathParams.forEach(param => delete params[param]);
146 // if authClient is actually a string, use it as an API KEY
147 if (typeof authClient === 'string') {
148 params.key = params.key || authClient;
149 authClient = undefined;
150 }
151 function multipartUpload(multipart) {
152 const boundary = uuid.v4();
153 const finale = `--${boundary}--`;
154 const rStream = new stream.PassThrough({
155 flush(callback) {
156 this.push('\r\n');
157 this.push(finale);
158 callback();
159 },
160 });
161 const pStream = new ProgressStream();
162 const isStream = isReadableStream(multipart[1].body);
163 headers['content-type'] = `multipart/related; boundary=${boundary}`;
164 for (const part of multipart) {
165 const preamble = `--${boundary}\r\ncontent-type: ${part['content-type']}\r\n\r\n`;
166 rStream.push(preamble);
167 if (typeof part.body === 'string') {
168 rStream.push(part.body);
169 rStream.push('\r\n');
170 }
171 else {
172 // Gaxios does not natively support onUploadProgress in node.js.
173 // Pipe through the pStream first to read the number of bytes read
174 // for the purpose of tracking progress.
175 pStream.on('progress', bytesRead => {
176 if (options.onUploadProgress) {
177 options.onUploadProgress({ bytesRead });
178 }
179 });
180 part.body.pipe(pStream).pipe(rStream);
181 }
182 }
183 if (!isStream) {
184 rStream.push(finale);
185 rStream.push(null);
186 }
187 options.data = rStream;
188 }
189 function browserMultipartUpload(multipart) {
190 const boundary = uuid.v4();
191 const finale = `--${boundary}--`;
192 headers['content-type'] = `multipart/related; boundary=${boundary}`;
193 let content = '';
194 for (const part of multipart) {
195 const preamble = `--${boundary}\r\ncontent-type: ${part['content-type']}\r\n\r\n`;
196 content += preamble;
197 if (typeof part.body === 'string') {
198 content += part.body;
199 content += '\r\n';
200 }
201 }
202 content += finale;
203 options.data = content;
204 }
205 if (parameters.mediaUrl && media.body) {
206 options.url = parameters.mediaUrl;
207 if (resource) {
208 params.uploadType = 'multipart';
209 const multipart = [
210 { 'content-type': 'application/json', body: JSON.stringify(resource) },
211 {
212 'content-type': media.mimeType || (resource && resource.mimeType) || defaultMime,
213 body: media.body,
214 },
215 ];
216 if (!(0, isbrowser_1.isBrowser)()) {
217 // gaxios doesn't support multipart/related uploads, so it has to
218 // be implemented here.
219 multipartUpload(multipart);
220 }
221 else {
222 browserMultipartUpload(multipart);
223 }
224 }
225 else {
226 params.uploadType = 'media';
227 Object.assign(headers, { 'content-type': media.mimeType || defaultMime });
228 options.data = media.body;
229 }
230 }
231 else {
232 options.data = resource || undefined;
233 }
234 options.headers = extend(true, options.headers || {}, headers);
235 options.params = params;
236 if (!(0, isbrowser_1.isBrowser)()) {
237 options.headers['Accept-Encoding'] = 'gzip';
238 options.userAgentDirectives.push({
239 product: 'google-api-nodejs-client',
240 version: pkg.version,
241 comment: 'gzip',
242 });
243 const userAgent = options.userAgentDirectives
244 .map(d => {
245 let line = `${d.product}/${d.version}`;
246 if (d.comment) {
247 line += ` (${d.comment})`;
248 }
249 return line;
250 })
251 .join(' ');
252 options.headers['User-Agent'] = userAgent;
253 }
254 // By default gaxios treats any 2xx as valid, and all non 2xx status
255 // codes as errors. This is a problem for HTTP 304s when used along
256 // with an eTag.
257 if (!options.validateStatus) {
258 options.validateStatus = status => {
259 return (status >= 200 && status < 300) || status === 304;
260 };
261 }
262 // Retry by default
263 options.retry = options.retry === undefined ? true : options.retry;
264 delete options.auth; // is overridden by our auth code
265 // Determine TPC universe
266 if (options.universeDomain &&
267 options.universe_domain &&
268 options.universeDomain !== options.universe_domain) {
269 throw new Error('Please set either universe_domain or universeDomain, but not both.');
270 }
271 const universeDomainEnvVar = typeof process === 'object' && typeof process.env === 'object'
272 ? process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN']
273 : undefined;
274 const universeDomain = (_d = (_c = (_b = options.universeDomain) !== null && _b !== void 0 ? _b : options.universe_domain) !== null && _c !== void 0 ? _c : universeDomainEnvVar) !== null && _d !== void 0 ? _d : 'googleapis.com';
275 // Update URL to point to the given TPC universe
276 if (universeDomain !== 'googleapis.com' && options.url) {
277 const url = new URL(options.url);
278 if (url.hostname.endsWith('.googleapis.com')) {
279 url.hostname = url.hostname.replace(/googleapis\.com$/, universeDomain);
280 options.url = url.toString();
281 }
282 }
283 // Perform the HTTP request. NOTE: this function used to return a
284 // mikeal/request object. Since the transition to Axios, the method is
285 // now void. This may be a source of confusion for users upgrading from
286 // version 24.0 -> 25.0 or up.
287 if (authClient && typeof authClient === 'object') {
288 // Validate TPC universe
289 const universeFromAuth = typeof authClient.getUniverseDomain === 'function'
290 ? await authClient.getUniverseDomain()
291 : undefined;
292 if (universeFromAuth && universeDomain !== universeFromAuth) {
293 throw new Error(`The configured universe domain (${universeDomain}) does not match the universe domain found in the credentials (${universeFromAuth}). ` +
294 "If you haven't configured the universe domain explicitly, googleapis.com is the default.");
295 }
296 if (options.http2) {
297 const authHeaders = await authClient.getRequestHeaders(options.url);
298 const mooOpts = Object.assign({}, options);
299 mooOpts.headers = Object.assign(mooOpts.headers, authHeaders);
300 return h2.request(mooOpts);
301 }
302 else {
303 return authClient.request(options);
304 }
305 }
306 else {
307 return new google_auth_library_1.DefaultTransporter().request(options);
308 }
309}
310/**
311 * Basic Passthrough Stream that records the number of bytes read
312 * every time the cursor is moved.
313 */
314class ProgressStream extends stream.Transform {
315 constructor() {
316 super(...arguments);
317 this.bytesRead = 0;
318 }
319 // eslint-disable-next-line @typescript-eslint/no-explicit-any
320 _transform(chunk, encoding, callback) {
321 this.bytesRead += chunk.length;
322 this.emit('progress', this.bytesRead);
323 this.push(chunk);
324 callback();
325 }
326}
327function populateAPIHeader(headers) {
328 // TODO: we should eventually think about adding browser support for this
329 // populating the gl-web header (web support should also be added to
330 // google-auth-library-nodejs).
331 if (!(0, isbrowser_1.isBrowser)()) {
332 headers['x-goog-api-client'] = `gdcl/${pkg.version} gl-node/${process.versions.node}`;
333 }
334}
335//# sourceMappingURL=apirequest.js.map
\No newline at end of file