UNPKG

azure

Version:
311 lines (288 loc) 15.5 kB
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. 'use strict'; // Module dependencies. const utils = require('./utils'); const Constants = require('./constants'); const HttpConstants = Constants.HttpConstants; const HttpVerbs = HttpConstants.HttpVerbs; const serializer = require('./serialization'); /** * Creates a new WebResource object. * * This class provides an abstraction over a REST call by being library / implementation agnostic and wrapping the necessary * properties to initiate a request. * * @constructor */ class WebResource { constructor() { this.rawResponse = false; this.queryString = {}; this.url = null; this.method = null; this.headers = {}; this.body = null; } /** * Hook up the given input stream to a destination output stream if the WebResource method * requires a request body and a body is not already set. * * @param {Stream} inputStream the stream to pipe from * @param {Stream} outputStream the stream to pipe to * * @return destStream */ pipeInput(inputStream, destStream) { function isMethodWithBody(verb) { return verb === HttpVerbs.PUT || verb === HttpVerbs.POST || verb === HttpVerbs.MERGE; } if (isMethodWithBody(this.method) && !this.hasOwnProperty('body')) { inputStream.pipe(destStream); } return destStream; } /** * Validates that the required properties such as method, url, headers['Content-Type'], * headers['accept-language'] are defined. It will throw an error if one of the above * mentioned properties are not defined. */ validateRequestProperties() { if (!this.method || !this.url || !this.headers['Content-Type'] || !this.headers['accept-language']) { throw new Error('method, url, headers[\'Content-Type\'], headers[\'accept-language\'] are ' + 'required properties before making a request. Either provide them or use WebResource.prepare() method.'); } } /** * Prepares the request. * * @param {object} options The request options that should be provided to send a request successfully * * @param {string} options.method The HTTP request method. Valid values are 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'POST', 'PATCH'. * * @param {string} [options.url] The request url. It may or may not have query parameters in it. * Either provide the 'url' or provide the 'pathTemplate' in the options object. Both the options are mutually exclusive. * * @param {object} [options.queryParameters] A dictionary of query parameters to be appended to the url, where * the 'key' is the 'query-parameter-name' and the 'value' is the 'query-parameter-value'. * The 'query-parameter-value' can be of type 'string' or it can be of type 'object'. * The 'object' format should be used when you want to skip url encoding. While using the object format, * the object must have a property named value which provides the 'query-parameter-value'. * Example: * - query-parameter-value in 'object' format: { 'query-parameter-name': { value: 'query-parameter-value', skipUrlEncoding: true } } * - query-parameter-value in 'string' format: { 'query-parameter-name': 'query-parameter-value'}. * Note: 'If options.url already has some query parameters, then the value provided in options.queryParameters will be appended to the url. * * @param {string} [options.pathTemplate] The path template of the request url. Either provide the 'url' or provide the 'pathTemplate' * in the options object. Both the options are mutually exclusive. * Example: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}' * * @param {string} [options.baseUrl] The base url of the request. Default value is: 'https://management.azure.com'. This is applicable only with * options.pathTemplate. If you are providing options.url then it is expected that you provide the complete url. * * @param {object} [options.pathParameters] A dictionary of path parameters that need to be replaced with actual values in the pathTemplate. * Here the key is the 'path-parameter-name' and the value is the 'path-parameter-value'. * The 'path-parameter-value' can be of type 'string' or it can be of type 'object'. * The 'object' format should be used when you want to skip url encoding. While using the object format, * the object must have a property named value which provides the 'path-parameter-value'. * Example: * - path-parameter-value in 'object' format: { 'path-parameter-name': { value: 'path-parameter-value', skipUrlEncoding: true } } * - path-parameter-value in 'string' format: { 'path-parameter-name': 'path-parameter-value' }. * * @param {object} [options.formData] A dictionary of key-value pairs for the formData object. * If the expected 'Content-Type' to be set is 'application/x-www-form-urlencoded' then please set it in the options.headers object else the * 'Content-Type' header will be set to 'multipart/form-data'. * * @param {object} [options.headers] A dictionary of request headers that need to be applied to the request. * Here the key is the 'header-name' and the value is the 'header-value'. The header-value MUST be of type string. * - ContentType must be provided with the key name as 'Content-Type'. Default value 'application/json; charset=utf-8'. * - 'Transfer-Encoding' is set to 'chunked' by default if 'options.bodyIsStream' is set to true. * - 'Content-Type' is set to 'application/octet-stream' by default if 'options.bodyIsStream' is set to true. * - 'accept-language' by default is set to 'en-US' * - 'x-ms-client-request-id' by default is set to a new Guid. To not generate a guid for the request, please set options.disableClientRequestId to true * * @param {boolean} [options.disableClientRequestId] When set to true, instructs the client to not set 'x-ms-client-request-id' header to a new Guid(). * * @param {object|string|boolean|array|number|null|undefined} [options.body] - The request body. It can be of any type. This method will JSON.stringify() the request body. * * @param {object} [options.serializationMapper] - Provides information on how to serialize the request body. * * @param {object} [options.deserializationMapper] - Provides information on how to deserialize the response body. * * @param {boolean} [options.disableJsonStringifyOnBody] - Indicates whether this method should JSON.stringify() the request body. Default value: false. * * @param {boolean} [options.bodyIsStream] - Indicates whether the request body is a stream (useful for file upload scenarios). * * @returns {object} WebResource Returns the prepared WebResource (HTTP Request) object that needs to be given to the request pipeline. */ prepare(options) { if (options === null || options === undefined || typeof options !== 'object') { throw new Error('options cannot be null or undefined and must be of type object'); } if (options.method === null || options.method === undefined || typeof options.method.valueOf() !== 'string') { throw new Error('options.method cannot be null or undefined and it must be of type string.'); } if (options.url && options.pathTemplate) { throw new Error('options.url and options.pathTemplate are mutually exclusive. Please provide either of them.'); } if ((options.pathTemplate === null || options.pathTemplate === undefined || typeof options.pathTemplate.valueOf() !== 'string') && (options.url === null || options.url === undefined || typeof options.url.valueOf() !== 'string')) { throw new Error('Please provide either options.pathTemplate or options.url. Currently none of them were provided.'); } //set the url if it is provided. if (options.url) { if (typeof options.url !== 'string') { throw new Error('options.url must be of type \'string\'.'); } this.url = options.url; } //set the method if (options.method) { let validMethods = ['GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'POST', 'PATCH', 'TRACE']; if (validMethods.indexOf(options.method.toUpperCase()) === -1) { throw new Error('The provided method \'' + options.method + '\' is invalid. Supported HTTP methods are: ' + JSON.stringify(validMethods)); } } this.method = options.method.toUpperCase(); //construct the url if path template is provided if (options.pathTemplate) { if (typeof options.pathTemplate !== 'string') { throw new Error('options.pathTemplate must be of type \'string\'.'); } if (!options.baseUrl) { options.baseUrl = 'https://management.azure.com'; } let baseUrl = options.baseUrl; let url = baseUrl + (baseUrl.endsWith('/') ? '' : '/') + (options.pathTemplate.startsWith('/') ? options.pathTemplate.slice(1) : options.pathTemplate); let segments = url.match(/({\w*\s*\w*})/ig); if (segments && segments.length) { if (options.pathParameters === null || options.pathParameters === undefined || typeof options.pathParameters !== 'object') { throw new Error(`pathTemplate: ${options.pathTemplate} has been provided. Hence, options.pathParameters ` + `cannot be null or undefined and must be of type "object".`); } segments.forEach(function (item) { let pathParamName = item.slice(1, -1); let pathParam = options.pathParameters[pathParamName]; if (pathParam === null || pathParam === undefined || !(typeof pathParam === 'string' || typeof pathParam === 'object')) { throw new Error(`pathTemplate: ${options.pathTemplate} contains the path parameter ${pathParamName}` + ` however, it is not present in ${options.pathParameters} - ${JSON.stringify(options.pathParameters, null, 2)}.` + `The value of the path parameter can either be a "string" of the form { ${pathParamName}: "some sample value" } or ` + `it can be an "object" of the form { "${pathParamName}": { value: "some sample value", skipUrlEncoding: true } }.`); } if (typeof pathParam.valueOf() === 'string') { url = url.replace(item, encodeURIComponent(pathParam)); } if (typeof pathParam.valueOf() === 'object') { if (!pathParam.value) { throw new Error(`options.pathParameters[${pathParamName}] is of type "object" but it does not contain a "value" property.`); } if (pathParam.skipUrlEncoding) { url = url.replace(item, pathParam.value); } else { url = url.replace(item, encodeURIComponent(pathParam.value)); } } }); } this.url = url; } //append query parameters to the url if they are provided. They can be provided with pathTemplate or url option. if (options.queryParameters) { if (typeof options.queryParameters !== 'object') { throw new Error(`options.queryParameters must be of type object. It should be a JSON object ` + `of "query-parameter-name" as the key and the "query-parameter-value" as the value. ` + `The "query-parameter-value" may be fo type "string" or an "object" of the form { value: "query-parameter-value", skipUrlEncoding: true }.`); } //append question mark if it is not present in the url if (this.url && this.url.indexOf('?') === -1) { this.url += '?'; } //construct queryString let queryParams = []; let queryParameters = options.queryParameters; //We need to populate this.query as a dictionary if the request is being used for Sway's validateRequest(). this.query = {}; for (let queryParamName in queryParameters) { let queryParam = queryParameters[queryParamName]; if (queryParam) { if (typeof queryParam === 'string') { queryParams.push(queryParamName + '=' + encodeURIComponent(queryParam)); this.query[queryParamName] = encodeURIComponent(queryParam); } else if (typeof queryParam === 'object') { if (!queryParam.value) { throw new Error(`options.queryParameters[${queryParamName}] is of type "object" but it does not contain a "value" property.`); } if (queryParam.skipUrlEncoding) { queryParams.push(queryParamName + '=' + queryParam.value); this.query[queryParamName] = queryParam.value; } else { queryParams.push(queryParamName + '=' + encodeURIComponent(queryParam.value)); this.query[queryParamName] = encodeURIComponent(queryParam.value); } } } }//end-of-for //append the queryString this.url += queryParams.join('&'); } //set formData parameters for 'application/x-www-form-urlencoded' or 'multipart/form-data'. if (options.formData) { if (options.headers && options.headers['Content-Type'] === 'application/x-www-form-urlencoded') { this.form = options.formData; this.headers['Content-Type'] = 'application/x-www-form-urlencoded'; } else { this.formData = options.formData; this.headers['Content-Type'] = 'multipart/form-data'; } } //add headers to the request if they are provided if (options.headers) { let headers = options.headers; for (let headerName in headers) { if (headers.hasOwnProperty(headerName)) { this.headers[headerName] = headers[headerName]; } } } //ensure accept-language is set correctly if (!this.headers['accept-language']) { this.headers['accept-language'] = 'en-US'; } //ensure the request-id is set correctly if (!this.headers['x-ms-client-request-id'] && !options.disableClientRequestId) { this.headers['x-ms-client-request-id'] = utils.generateUuid(); } //default if (!this.headers['Content-Type']) { this.headers['Content-Type'] = 'application/json; charset=utf-8'; } //set the request body. request.js automatically sets the Content-Length request header, so we need not set it explicilty this.body = null; if (options.body !== null && options.body !== undefined) { //body as a stream special case. set the body as-is and check for some special request headers specific to sending a stream. if (options.bodyIsStream) { this.body = options.body; if (!this.headers['Transfer-Encoding']) { this.headers['Transfer-Encoding'] = 'chunked'; } if (this.headers['Content-Type'] !== 'application/octet-stream') { this.headers['Content-Type'] = 'application/octet-stream'; } } else { let serializedBody = null; if (options.serializationMapper) { serializedBody = serializer.serialize(options.serializationMapper, options.body, 'requestBody'); } if (options.disableJsonStringifyOnBody) { this.body = serializedBody || options.body; } else { this.body = serializedBody ? JSON.stringify(serializedBody) : JSON.stringify(options.body); } } } return this; } } module.exports = WebResource;