diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 75da21dd8639..fe5101ce630b 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -165,6 +165,7 @@ dependencies: tunnel: 0.0.6 typescript: 3.5.3 uglify-js: 3.6.0 + underscore: 1.4.4 url: 0.11.0 util: 0.12.1 uuid: 3.3.3 @@ -174,6 +175,7 @@ dependencies: ws: 7.1.2 xhr-mock: 2.5.0 xml2js: 0.4.19 + xmlbuilder: 0.4.3 yargs: 13.3.0 yarn: 1.17.3 lockfileVersion: 5.1 @@ -9024,6 +9026,10 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + /underscore/1.4.4: + dev: false + resolution: + integrity: sha1-YaajIBBiKvoHljvzJSA88SI51gQ= /underscore/1.8.3: dev: false resolution: @@ -9525,6 +9531,12 @@ packages: dev: false resolution: integrity: sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + /xmlbuilder/0.4.3: + dev: false + engines: + node: '>=0.2.0' + resolution: + integrity: sha1-xGFLp04K0ZbmCcknLNnh3bKKilg= /xmlbuilder/8.2.2: dev: false engines: @@ -9793,7 +9805,7 @@ packages: dev: false name: '@rush-temp/abort-controller' resolution: - integrity: sha512-bOazCplSYAusToKu3B9vUnNASGvMSR9Yyj4i1PFTGdOfMyd5t+VZcS6RgEdtcAr+6MoUwJ230UkC/7JZ+HkdVQ== + integrity: sha512-W9CsoX+0a+9AAHje89SaMJSq4zFtlcVwA8KRQR6nf+e0aSBjmIiU4NAZUfRwvXE11vWhjo+YUcV2+ISBt/UvWQ== tarball: 'file:projects/abort-controller.tgz' version: 0.0.0 'file:projects/core-amqp.tgz': @@ -9861,7 +9873,7 @@ packages: dev: false name: '@rush-temp/core-amqp' resolution: - integrity: sha512-zOQxTAzv2RDm8YmYZ8Tr+y82OmJANT0b3iJuBg+e02gcCSAS7N57zjeaZYSjq6dNEND7Jseo+xVZ/QdDNVGIsA== + integrity: sha512-jJ88adCOOKofY9LnfVCihOXMnB467Q3PLZuj4GmJpM51DyFx0KdCic1n3TqTj7SQ9sj1rKtVWyVAVAaOJRwTTA== tarball: 'file:projects/core-amqp.tgz' version: 0.0.0 'file:projects/core-arm.tgz': @@ -9896,7 +9908,7 @@ packages: dev: false name: '@rush-temp/core-arm' resolution: - integrity: sha512-9uqsxSLJiO0MSCXGkCe42Kf/kJO0w7GuYc4efzzjMJOsbsjBfu4wpt5IJOyzMUznyaPBFKOiR8wKboyhNfbzmg== + integrity: sha512-GVZI9FYYYQ4Jt0BkIi0OJ5vhcKGeoUhaUA8mw3XXNBvERXJqBZSpiWiJ03lXbnEgWvcz4Yn2EvgiWT0JSj2gkw== tarball: 'file:projects/core-arm.tgz' version: 0.0.0 'file:projects/core-asynciterator-polyfill.tgz': @@ -9914,7 +9926,7 @@ packages: dev: false name: '@rush-temp/core-asynciterator-polyfill' resolution: - integrity: sha512-X2qR67x/Wgwcs8VjH/m3EA7HFB22mL2sf9YJCvpdIUXvz5ENGH54U4TC8IAc2AxcqlKjOi1pwFUAf9yBf4nAJQ== + integrity: sha512-hp2FjyoaLo9iJIGV0Aq7UI8oGzIr29OewVSFfuckKI+2aDUrh6Kamlg/hw/HrZ/a3ybccP3oqGjwA2uze4DoAg== tarball: 'file:projects/core-asynciterator-polyfill.tgz' version: 0.0.0 'file:projects/core-auth.tgz': @@ -9952,7 +9964,7 @@ packages: dev: false name: '@rush-temp/core-auth' resolution: - integrity: sha512-WcJPwF+dNlpEQW1GRzGQIY6YEI+uz6ChF3PISTFVyFbZjK1k+5r0ly2DRCplHUd6tVJ2GoqbqmSHR8Doz/E4Mw== + integrity: sha512-oVJYTPPuUZ3iD4kJtFBd0lRmS1aca8qhLXqlV3TtVIIw/MHZ+kQiFiy/bJTwjSpxOWzL4b4l8yeg2dAXjyuROg== tarball: 'file:projects/core-auth.tgz' version: 0.0.0 'file:projects/core-http.tgz': @@ -10025,17 +10037,19 @@ packages: tunnel: 0.0.6 typescript: 3.5.3 uglify-js: 3.6.0 + underscore: 1.4.4 uuid: 3.3.3 webpack: 4.39.2_webpack@4.39.2 webpack-cli: 3.3.7_webpack@4.39.2 webpack-dev-middleware: 3.7.0_webpack@4.39.2 xhr-mock: 2.5.0 xml2js: 0.4.19 + xmlbuilder: 0.4.3 yarn: 1.17.3 dev: false name: '@rush-temp/core-http' resolution: - integrity: sha512-tnBnNKZPZvmwesatSPql50OdGqX9w/Hi2qEbvu2ATsp93+M6E5Ofg37iul2dlUdzwkfwa3tX97eTikU96MyvXw== + integrity: sha512-jZg32+noo1+LHfWEkWzu/cwN4vaxXenspeoozEwNiA3hgD7PI2DKZfrbVBOp1zj6Wp3VSkG3Kv2NydKXm971rw== tarball: 'file:projects/core-http.tgz' version: 0.0.0 'file:projects/core-paging.tgz': @@ -10054,7 +10068,7 @@ packages: dev: false name: '@rush-temp/core-paging' resolution: - integrity: sha512-/l5SA2u/jUrYjvQBf24YFdaoYW4GAdU3iDoGDekLbjH2fSP8wOt/3oDlBM8pxGD54TB7s06Oi4OjBsQN6F5tgw== + integrity: sha512-rorSx6Oeq/VsKWv3L8qwnHYihYjPZS/PiCZZrIlGt7WfSH8iS60XFPxRrAiP+zIhosvd1hDlSmQ8k30iIB10QA== tarball: 'file:projects/core-paging.tgz' version: 0.0.0 'file:projects/core-tracing.tgz': @@ -10092,7 +10106,7 @@ packages: dev: false name: '@rush-temp/core-tracing' resolution: - integrity: sha512-yAQfqHw4bMOPRFf2Q0SGJtgtdw+RPhMQtkNBh1s8TO5kk1HV7cE6S4Fh3kH3msRmRFM3M9yLmdcVt5asCcRjtg== + integrity: sha512-CRGjX9TjXCkXJMbYqwOpJxxMCDPq3ZUqJML0wdJD/1dUX84mu9aD0iMi6QjPxMPiuYmlvbqJ+aLaNXcZPoNY3A== tarball: 'file:projects/core-tracing.tgz' version: 0.0.0 'file:projects/event-hubs.tgz': @@ -10168,7 +10182,7 @@ packages: dev: false name: '@rush-temp/event-hubs' resolution: - integrity: sha512-ij9RrsZTCY20A1azLf+CQ2/sC3oz8FwzxNv+of934zmASFqNxTqwylat1zRPj/zQ+5WHpfq5BoNmXpTgshzzRg== + integrity: sha512-TMdz6KTaNgEH8tAeEntd48I6dIV5yZbYrDxgW0cLV4eR+agvLKFa3XPx6H8iiwF/Jyn/eMKu56iLP/nwnxUVFw== tarball: 'file:projects/event-hubs.tgz' version: 0.0.0 'file:projects/event-processor-host.tgz': @@ -10225,7 +10239,7 @@ packages: dev: false name: '@rush-temp/event-processor-host' resolution: - integrity: sha512-7/7AixJ0rDkzWSL4l0i3DbufYn8MgykQREWpVm5pTMaNi6HqQZGwCMSyCiVqRBSheWcn+shtaCNlRTAOA7nw2g== + integrity: sha512-c/puCLm/oWyCMxHOUHQ3ZszUEyoBpeoPBnpMiLYhdqGIl1cXBYxOzXJaN3MN4EgzztvbG/kUtOZt2h+tAoP7NA== tarball: 'file:projects/event-processor-host.tgz' version: 0.0.0 'file:projects/identity.tgz': @@ -10277,7 +10291,7 @@ packages: dev: false name: '@rush-temp/identity' resolution: - integrity: sha512-zBuzYgT0J+3USAuHuU/4bUU5W++5Fsp3U747YIjXOR2tSbiX9QVv9DKUgtJD8lQm1jdvZduzwIhMBTJd68lk8Q== + integrity: sha512-aVvXmz3WeYJqmGGqVVfQOL30VQi09y/31viVZ6CfXy0MzUtrSmCwX9sfKDR3RnbvjtULGivZvJp7OY5Pwv2EPw== tarball: 'file:projects/identity.tgz' version: 0.0.0 'file:projects/keyvault-certificates.tgz': @@ -10345,7 +10359,7 @@ packages: dev: false name: '@rush-temp/keyvault-certificates' resolution: - integrity: sha512-RGwQyIALGePUl3hTCS4uu9E8RIKUpOvv/MYZDgwfQOo5ZvGrWN1HkDd0OaafAnfDFFM7aalGt1JnZBh9RCGFFQ== + integrity: sha512-8YOlO6QG3YkvNcFPbJL8nbroNLJpDcxGdBXukPxgNcw74wWIEGACMb77FRZbsviTl4Fm882+yJTVl6q4xNZHww== tarball: 'file:projects/keyvault-certificates.tgz' version: 0.0.0 'file:projects/keyvault-keys.tgz': @@ -10414,7 +10428,7 @@ packages: dev: false name: '@rush-temp/keyvault-keys' resolution: - integrity: sha512-jxZzHCbuJfaD39hHIzP3fyE2eidbNN7iXcHNptZk8qt+rp9E0d5MgpGrgdFbqVPFhUl5AqAbrRfUlOdYvvHR2Q== + integrity: sha512-DjVpS2TWUSIbp4acVsnz9mklUM/foZjjrVIs1Vo86HzF28igiFanj74BoWmiVzfubanC9+KwdEjQQ8U9CsWIXA== tarball: 'file:projects/keyvault-keys.tgz' version: 0.0.0 'file:projects/keyvault-secrets.tgz': @@ -10481,7 +10495,7 @@ packages: dev: false name: '@rush-temp/keyvault-secrets' resolution: - integrity: sha512-KKRhMhKadWkVD6YNbv0oc5hUwWXePFshonrEk8sPqWCJy2wmxDQcByw5kXfvu4ff1pBu5YU2/WGJea+78lvg4Q== + integrity: sha512-Sml9gYtRdzXKZ/tiXOrFJebP7VayBC8dZmiCGWeypwyeME7KfNUu7RS1/SoVL3FICnoG9DGBOhuW4Hitmzx8yA== tarball: 'file:projects/keyvault-secrets.tgz' version: 0.0.0 'file:projects/service-bus.tgz': @@ -10558,7 +10572,7 @@ packages: dev: false name: '@rush-temp/service-bus' resolution: - integrity: sha512-afJ8wnIxTmCpYPotxye4XswCWAlNTlXQIU8MJh7Tf0YRxLZXtUZSndz8Z9LjXcKyOJst5u1Cub0GTONIs0ZS9Q== + integrity: sha512-FwSxg/1Ynbnbyna5nzRpMryYhlKwPQagswhSvdmTEBT14skEz2wxcO1ZCalQL3yUHxtLqNTbwSEWs/m1iwVRbQ== tarball: 'file:projects/service-bus.tgz' version: 0.0.0 'file:projects/storage-blob.tgz': @@ -10628,7 +10642,7 @@ packages: dev: false name: '@rush-temp/storage-blob' resolution: - integrity: sha512-hxiIH/jGLBocF8uWR2vDOwx7w+bFJvNGW/k+Ds1FDZxQatiZLLgQvutwzTLc2oKNBxUrXKssQGIG7e7oHl08+A== + integrity: sha512-kn2byrHTxQIDWXEwQS14S523e/NgrUkWWdtk/EyQb11NWHZJGzqWOR7soCXQgZfmWIGgjKfPFM/Xwrqf3vECMA== tarball: 'file:projects/storage-blob.tgz' version: 0.0.0 'file:projects/storage-file.tgz': @@ -10698,7 +10712,7 @@ packages: dev: false name: '@rush-temp/storage-file' resolution: - integrity: sha512-Om0AwUAkaQ2MXCiO3Tbr9o7dAHIAORNMEGG/vHmjsQT1TcMnQtxkj6W3Vi12arC7Befc2eRBK6qCx4dmAWUc0g== + integrity: sha512-oj8bSAecZKtBXtPot5VqiCMqKQx5sQIjbNgGg1IyGwsdfgeTJKgMYOJpPK8MSMqvYK9EZudw3jkeFOIA378j1w== tarball: 'file:projects/storage-file.tgz' version: 0.0.0 'file:projects/storage-queue.tgz': @@ -10767,7 +10781,7 @@ packages: dev: false name: '@rush-temp/storage-queue' resolution: - integrity: sha512-emEeCElwVRQ9uXdWyORLRGge2NTOWtKaZ1UNKrzC8EOToJuJapl7SG6SxCpvXuLOxVyhMXLQCOx+IRAWLi9XlA== + integrity: sha512-73zOl+s2xrm/5cQo1kRzdupjV451qFHM1SMEHnZMNG5uX8NlLPFy3A5qPcj5dGcrBwdA33JdZKYhMkLCLk48fQ== tarball: 'file:projects/storage-queue.tgz' version: 0.0.0 'file:projects/template.tgz': @@ -10817,7 +10831,7 @@ packages: dev: false name: '@rush-temp/template' resolution: - integrity: sha512-CT4vFqfIOTwrzlk0gsSp71MNMAgA6cbt4Sa3BkCVhr81N6oXnvIoGIMCtzSerpvI8j1auYc0yM4rKldxNSFTqA== + integrity: sha512-gMekh0FZCuSys6TamLN8h1nt7hJ/ah6Ryu7nR26ETWqXzxQnsnZrq27JRWgA2x5QFv5XBXXZJayhpUId9K+8bA== tarball: 'file:projects/template.tgz' version: 0.0.0 'file:projects/testhub.tgz': @@ -10838,9 +10852,10 @@ packages: dev: false name: '@rush-temp/testhub' resolution: - integrity: sha512-VxrbDXfuJ6Nz4rm0DHlJ+0sMk4RMKRflIyu7WxXLZGBpri9KLivFyNA0TWfZBifpdy3T1kVXyLOccskpzczDvA== + integrity: sha512-5P+X3IwgFa9rlq+jbtHxWVZWF1kDmRZwV8jQfHMYV5CLsJjXKw1yISIRSFmCmuXDdHYEnxO1bnRGi34QeZ+63g== tarball: 'file:projects/testhub.tgz' version: 0.0.0 +registry: '' specifiers: '@azure/amqp-common': 1.0.0-preview.6 '@azure/arm-servicebus': ^3.2.0 @@ -11008,6 +11023,7 @@ specifiers: tunnel: ^0.0.6 typescript: ^3.2.2 uglify-js: ^3.4.9 + underscore: 1.4.x url: ^0.11.0 util: ^0.12.1 uuid: ^3.3.2 @@ -11017,5 +11033,6 @@ specifiers: ws: ^7.1.1 xhr-mock: ^2.4.1 xml2js: ^0.4.19 + xmlbuilder: 0.4.3 yargs: ^13.0.0 yarn: ^1.6.0 diff --git a/sdk/core/core-http/lib/atomResourceSerializerBase.ts b/sdk/core/core-http/lib/atomResourceSerializerBase.ts new file mode 100644 index 000000000000..33b8abaac5dd --- /dev/null +++ b/sdk/core/core-http/lib/atomResourceSerializerBase.ts @@ -0,0 +1,174 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { stringIsEmpty, stringStartsWith } from "./util/utils"; +import { Constants } from "./util/constants"; +import { AtomHandler } from "./util/atomHandler"; +import { each, isUndefined, isArray, isObject, isDate } from "./util/utils"; +const url = require("url"); +const xmlbuilder = require("xmlbuilder"); + +const atomHandler = new AtomHandler(); + +export class AtomResourceSerializerBase { + static setName(entry: any, nameProperty: any): any { + var parsedUrl: any = url.parse(entry[Constants.ATOM_METADATA_MARKER].id); + var parts = parsedUrl.pathname!.split("/"); + + for (var i = 0; i * 2 < parts.length - 1; i++) { + entry[nameProperty[i]] = parts[i * 2 + 1]; + } + } + + static _serialize(resourceName: any, resource: any, properties: any): any { + var content: any = {}; + content[resourceName] = { + $: { + xmlns: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect" + } + }; + + if (resource) { + // Sort properties according to what is allowed by the service + each(properties, function(property: any): any { + if (!isUndefined(resource[property])) { + content[resourceName][property] = resource[property]; + } + }); + } + + return atomHandler.serializeEntry(content); + } + + static _parse(nameProperty: any, xml: any): any { + var result = atomHandler.parse(xml); + + if (!result) { + return undefined; + } + if (isArray(result)) { + each(result, function(entry: any): any { + AtomResourceSerializerBase.setName(entry, nameProperty); + }); + } else { + AtomResourceSerializerBase.setName(result, nameProperty); + } + return result; + } + + static serializeEntry(content: any, namespaces?: any, properties?: any): any { + var doc = xmlbuilder.create("entry").att("xmlns", "http://www.w3.org/2005/Atom"); + + each(namespaces, function(namespace: any): any { + doc = doc.att("xmlns:" + namespace.key, namespace.url); + }); + + if (properties) { + Object.keys(properties).forEach(function(property) { + doc = doc.ele(property, properties[property]).up(); + }); + } + + doc = doc.ele("updated", new Date().toISOString()).up(); + + content[Constants.XML_METADATA_MARKER] = { type: "application/xml" }; + + doc = AtomResourceSerializerBase._writeElementValue(doc, "content", content); + + return doc.doc().toString(); + } + + static _writeElementValue(parentElement: any, name: any, value: any): any { + var ignored = false; + var propertyTagName = name; + + if (!stringIsEmpty(value) && isObject(value) && !isDate(value)) { + if (Array.isArray(value) && value.length > 0) { + // Example: + // JSON: element: [ { property1: 'hi there' }, { property2: 'hello there' } ] + // XML: hi therehello there + + Object.keys(value).forEach(function(i: any) { + parentElement = AtomResourceSerializerBase._writeElementValue( + parentElement, + name, + value[i] + ); + }); + + // For an array no element was actually added at this level, so skip uping level. + ignored = true; + } else if ( + value[Constants.XML_METADATA_MARKER] !== undefined && + value[Constants.XML_VALUE_MARKER] !== undefined + ) { + // Example: + // JSON: element: { '$': { 'm:type' = 'Edm.String' }, '_': 'hi there' } + // XML: hi there + + parentElement = parentElement.ele(propertyTagName); + if (!stringIsEmpty(value[Constants.XML_VALUE_MARKER])) { + if (stringStartsWith(value[Constants.XML_VALUE_MARKER], "hi therehello there + + parentElement = parentElement.ele(propertyTagName); + for (var propertyName in value) { + if ( + propertyName !== Constants.XML_METADATA_MARKER && + value.hasOwnProperty(propertyName) + ) { + parentElement = AtomResourceSerializerBase._writeElementValue( + parentElement, + propertyName, + value[propertyName] + ); + } + } + } + } else { + parentElement = parentElement.ele(propertyTagName); + if (!stringIsEmpty(value)) { + if (stringStartsWith(value.toString().trim(), "} The signed request object. + */ + signRequest(webResource: WebResource): any { + if (!webResource.headers) webResource.headers = new HttpHeaders(); + + var targetUri = encodeURIComponent(webResource.url.toLowerCase()).toLowerCase(); + + let date = new Date(); + date.setMinutes(date.getMinutes() + 5); + var expirationDate = Math.round(date.valueOf() / 1000); + var signature = this._generateSignature(targetUri, expirationDate); + webResource.headers.set( + HeaderConstants.AUTHORIZATION, + util.format( + "SharedAccessSignature sig=%s&se=%s&skn=%s&sr=%s", + signature, + expirationDate, + this.keyName, + targetUri + ) + ); + + return Promise.resolve(webResource); + } +} diff --git a/sdk/core/core-http/lib/httpOperationResponse.ts b/sdk/core/core-http/lib/httpOperationResponse.ts index 9b30056b32df..26d325f712f5 100644 --- a/sdk/core/core-http/lib/httpOperationResponse.ts +++ b/sdk/core/core-http/lib/httpOperationResponse.ts @@ -29,7 +29,7 @@ declare global { * Stub declaration of the browser-only Blob type. * Full type information can be obtained by including "lib": ["dom"] in tsconfig.json. */ - interface Blob { } + interface Blob {} } /** @@ -52,6 +52,11 @@ export interface HttpOperationResponse extends HttpResponse { */ parsedBody?: any; + /** + * The error information in JSON or XML + */ + errorBody?: any; + /** * BROWSER ONLY * diff --git a/sdk/core/core-http/lib/policies/serviceBusAtomSerializationPolicy.ts b/sdk/core/core-http/lib/policies/serviceBusAtomSerializationPolicy.ts new file mode 100644 index 000000000000..d76c40faa14e --- /dev/null +++ b/sdk/core/core-http/lib/policies/serviceBusAtomSerializationPolicy.ts @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { HttpOperationResponse } from "../httpOperationResponse"; +import { WebResource } from "../webResource"; +import { + BaseRequestPolicy, + RequestPolicy, + RequestPolicyFactory, + RequestPolicyOptions +} from "./requestPolicy"; +import { Constants } from "../util/constants"; +import { parseAtomXML, parseStringError } from "../util/xml"; +import { isString, extend } from "../util/utils"; +import { Buffer } from "buffer"; +import { ResourceSerializer } from "../resourceSerializer"; + +/** + * Create a new serialization RequestPolicyCreator that will serialize/deserialize + * HTTP request bodies as they pass through the HTTP pipeline. + */ +export function serviceBusAtomSerializationPolicy(): RequestPolicyFactory { + return { + create: (nextPolicy: RequestPolicy, options: RequestPolicyOptions) => { + return new ServiceBusAtomSerializationPolicy(nextPolicy, options); + } + }; +} + +/** + * A RequestPolicy that will + * - serialize HTTP requests with input in JSON to ATOM based XML requests, and + * - deserialize the ATOM based XML responses as they pass through the HTTP pipeline. + */ +export class ServiceBusAtomSerializationPolicy extends BaseRequestPolicy { + constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions) { + super(nextPolicy, options); + } + + public async sendRequest(request: WebResource): Promise { + let shouldParseResponse = false; + let serializer: ResourceSerializer; + if (request.atomXmlOperationSpec) { + serializer = request.atomXmlOperationSpec.serializer; + shouldParseResponse = request.atomXmlOperationSpec.shouldParseResponse; + if (request.body) { + request.body = serializer.serialize(JSON.parse(request.body)); + } + } + + return this._nextPolicy.sendRequest(request).then((response: HttpOperationResponse) => { + const parsedResponse: HttpOperationResponse = this.parseResponse(response); + + // Construct response with 'result' to be backward compatibile + const responseInCustomJson: any = { + error: parsedResponse.errorBody, + response: parsedResponse.parsedBody, + result: undefined + }; + responseInCustomJson.result = + shouldParseResponse && serializer + ? serializer.parse(responseInCustomJson.response) + : undefined; + + response.parsedBody = responseInCustomJson; + return response; + }); + } + + /** + * Process the response. + * @ignore + * + * @param {WebResource} webResource The web resource that made the request. + * @param {Response} response The response object. + * @return The normalized responseObject. + */ + private parseResponse(response: HttpOperationResponse): HttpOperationResponse { + const parsedResponse: HttpOperationResponse = this._parseXmlResponse(response); + + if (response.status >= 200 && response.status < 300) { + response.errorBody = undefined; + return response; + } + + const HttpResponseCodes: any = Constants.HttpResponseCodes; + + if (!parsedResponse.errorBody) { + var code = Object.keys(HttpResponseCodes).filter(function(name: any): any { + if (HttpResponseCodes[name] === response.status) { + return name; + } + }); + + parsedResponse.errorBody = { error: { code: code[0] } }; + } + var normalizedError = this._normalizeError(parsedResponse.errorBody, response); + parsedResponse.errorBody = normalizedError; + return parsedResponse; + } + + private _parseXmlResponse(response: HttpOperationResponse): HttpOperationResponse { + const parsedResponse = response; + try { + if (response.bodyAsText && Buffer.byteLength(response.bodyAsText.toString()) > 0) { + parsedResponse.parsedBody = parseAtomXML(response.bodyAsText); + } + } catch (e) { + parsedResponse.errorBody = { error: e }; + } + return this.parseUncategorizedResponse(parsedResponse); + } + + private parseUncategorizedResponse(response: HttpOperationResponse): HttpOperationResponse { + const parsedResponse = response; + try { + // Start by assuming XML + parsedResponse.parsedBody = parseAtomXML(response.bodyAsText); + } catch (e) { + // Try string if XML failed to parse a valid error xml + try { + parsedResponse.parsedBody = parseStringError(response.bodyAsText); + } catch (e) { + // Do nothing + } + } + return parsedResponse; + } + + _normalizeError(error: any, response: HttpOperationResponse): any { + if (isString(error)) { + return new Error(error); + } else if (error) { + var normalizedError: any = {}; + + var odataErrorFormat = !!error["odata.error"]; + var errorProperties = error.Error || error.error || error["odata.error"] || error; + if (odataErrorFormat) { + for (var property in errorProperties) { + if (errorProperties.hasOwnProperty(property)) { + var value = null; + if ( + property === Constants.ODATA_ERROR_MESSAGE && + !isString(errorProperties[Constants.ODATA_ERROR_MESSAGE]) + ) { + if ( + errorProperties[Constants.ODATA_ERROR_MESSAGE][Constants.ODATA_ERROR_MESSAGE_VALUE] + ) { + value = + errorProperties[Constants.ODATA_ERROR_MESSAGE][ + Constants.ODATA_ERROR_MESSAGE_VALUE + ]; + } else { + value = "missing value in the message property of the odata error format"; + } + } else { + value = errorProperties[property]; + } + normalizedError[property.toLowerCase()] = value; + } + } + } else { + for (var property in errorProperties) { + if (errorProperties.hasOwnProperty(property)) { + var value = null; + if (property !== "$") { + if (errorProperties[property] && errorProperties[property]["_"]) { + value = errorProperties[property]["_"]; + } else { + value = errorProperties[property]; + } + normalizedError[property.toLowerCase()] = value; + } + } + } + } + var errorMessage = normalizedError.code; + if (normalizedError.detail) { + errorMessage += " - " + normalizedError.detail; + } + + if (response) { + if (response.status) { + normalizedError.statusCode = response.status; + } + + if (response.headers && response.headers.get("x-ms-request-id")) { + normalizedError.requestId = response.headers.get("x-ms-request-id"); + } + } + + var errorObject = new Error(errorMessage); + return extend(errorObject, normalizedError); + } + + return undefined; + } +} diff --git a/sdk/core/core-http/lib/resourceSerializer.ts b/sdk/core/core-http/lib/resourceSerializer.ts new file mode 100644 index 000000000000..cccdd1e68922 --- /dev/null +++ b/sdk/core/core-http/lib/resourceSerializer.ts @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export abstract class ResourceSerializer { + abstract serialize(resource: any): any; + + abstract parse(xml: any): any; +} diff --git a/sdk/core/core-http/lib/util/atomHandler.ts b/sdk/core/core-http/lib/util/atomHandler.ts new file mode 100644 index 000000000000..6575932d0448 --- /dev/null +++ b/sdk/core/core-http/lib/util/atomHandler.ts @@ -0,0 +1,199 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Constants } from "./constants"; +import util from "util"; +import { isArray, each, stringIsEmpty, stringStartsWith, isDate, isObject } from "./utils"; +const xmlbuilder = require("xmlbuilder"); + +export class AtomHandler { + parseEntryResult(entry: any): any { + var contentElementName = Object.keys(entry.content).filter(function(key) { + return key !== Constants.XML_METADATA_MARKER; + })[0]; + + delete entry.content[contentElementName][Constants.XML_METADATA_MARKER]; + var result = entry.content[contentElementName]; + + if (result) { + if (entry[Constants.XML_METADATA_MARKER]) { + result[Constants.ATOM_METADATA_MARKER] = entry[Constants.XML_METADATA_MARKER]; + } else { + result[Constants.ATOM_METADATA_MARKER] = {}; + } + + result[Constants.ATOM_METADATA_MARKER]["ContentRootElement"] = contentElementName; + + for (var property in entry) { + if (property !== "content" && property !== Constants.XML_METADATA_MARKER) { + result[Constants.ATOM_METADATA_MARKER][property] = entry[property]; + } + } + } + + return result; + } + + parseFeedResult(feed: any): any { + var result = []; + var self = this; + if (feed.entry) { + if (isArray(feed.entry)) { + each(feed.entry, function(entry: any) { + result.push(self.parseEntryResult(entry)); + }); + } else { + result.push(self.parseEntryResult(feed.entry)); + } + } + return result; + } + + parse(xml: any): any { + var self = this; + if (!xml) { + return; + } + + if (xml.feed) { + return self.parseFeedResult(xml.feed); + } + + if (xml.entry) { + return self.parseEntryResult(xml.entry); + } + + throw new Error("Unrecognized result " + util.inspect(xml)); + } + + /** + * @param {object} content The content payload as it is to be serialized. It should include any root node(s). + * @param {array} namespaces An array of top level namespaces to be defined. + */ + serializeEntry(content: any, namespaces?: any, properties?: any): any { + var doc = xmlbuilder.create(); + + doc = doc + .begin("entry", { version: "1.0", encoding: "utf-8", standalone: "yes" }) + .att("xmlns", "http://www.w3.org/2005/Atom"); + + each(namespaces, function(namespace: any): any { + doc = doc.att("xmlns:" + namespace.key, namespace.url); + }); + + if (properties) { + Object.keys(properties).forEach(function(property) { + doc = doc.ele(property, properties[property]).up(); + }); + } + + doc = doc.ele("updated", new Date().toISOString()).up(); + + content[Constants.XML_METADATA_MARKER] = { type: "application/xml" }; + + doc = this._writeElementValue(doc, "content", content); + + return doc.doc().toString(); + } + + /* + * Writes a single property for an entry or complex type. + * + * @param {object} parentElement Parent DOM element under which the property should be added. + * @param {string} name Property name. + * @param {object} value Property value. + * @return {object} The current DOM element. + * + * {Deprecated} + */ + _writeElementValue(parentElement: any, name: any, value: any): any { + var self = this; + var ignored = false; + var propertyTagName = name; + + if (!stringIsEmpty(value) && isObject(value) && !isDate(value)) { + if (Array.isArray(value) && value.length > 0) { + // Example: + // JSON: element: [ { property1: 'hi there' }, { property2: 'hello there' } ] + // XML: hi therehello there + + Object.keys(value).forEach(function(i: any) { + parentElement = self._writeElementValue(parentElement, name, value[i]); + }); + + // For an array no element was actually added at this level, so skip uping level. + ignored = true; + } else if ( + value[Constants.XML_METADATA_MARKER] !== undefined && + value[Constants.XML_VALUE_MARKER] !== undefined + ) { + // Example: + // JSON: element: { '$': { 'm:type' = 'Edm.String' }, '_': 'hi there' } + // XML: hi there + + parentElement = parentElement.ele(propertyTagName); + if (!stringIsEmpty(value[Constants.XML_VALUE_MARKER])) { + if (stringStartsWith(value[Constants.XML_VALUE_MARKER], "hi therehello there + + parentElement = parentElement.ele(propertyTagName); + for (var propertyName in value) { + if ( + propertyName !== Constants.XML_METADATA_MARKER && + value.hasOwnProperty(propertyName) + ) { + parentElement = self._writeElementValue( + parentElement, + propertyName, + value[propertyName] + ); + } + } + } + } else { + parentElement = parentElement.ele(propertyTagName); + if (!stringIsEmpty(value)) { + if (stringStartsWith(value.toString().trim(), ", kickst * * @returns {object} Returns the merged target object. */ -export function mergeObjects(source: { [key: string]: any; }, target: { [key: string]: any; }) { +export function mergeObjects(source: { [key: string]: any }, target: { [key: string]: any }) { Object.keys(source).forEach((key) => { target[key] = source[key]; }); @@ -168,7 +180,12 @@ export interface ServiceCallback { * @param {WebResource} [request] The raw/actual request sent to the server if an error did not occur. * @param {HttpOperationResponse} [response] The raw/actual response from the server if an error did not occur. */ - (err: Error | RestError | null, result?: TResult, request?: WebResource, response?: HttpOperationResponse): void; + ( + err: Error | RestError | null, + result?: TResult, + request?: WebResource, + response?: HttpOperationResponse + ): void; } /** @@ -182,11 +199,14 @@ export function promiseToCallback(promise: Promise): Function { throw new Error("The provided input is not a Promise."); } return (cb: Function): void => { - promise.then((data: any) => { - cb(undefined, data); - }, (err: Error) => { - cb(err); - }); + promise.then( + (data: any) => { + cb(undefined, data); + }, + (err: Error) => { + cb(err); + } + ); }; } @@ -200,11 +220,14 @@ export function promiseToServiceCallback(promise: Promise): void => { - promise.then((data: HttpOperationResponse) => { - process.nextTick(cb, undefined, data.parsedBody as T, data.request, data); - }, (err: Error) => { - process.nextTick(cb, err); - }); + promise.then( + (data: HttpOperationResponse) => { + process.nextTick(cb, undefined, data.parsedBody as T, data.request, data); + }, + (err: Error) => { + process.nextTick(cb, err); + } + ); }; } @@ -221,8 +244,8 @@ export function prepareXMLRootList(obj: any, elementName: string) { * @param {Array} sourceCtors An array of source objects from which the properties need to be taken. */ export function applyMixins(targetCtor: any, sourceCtors: any[]): void { - sourceCtors.forEach(sourceCtors => { - Object.getOwnPropertyNames(sourceCtors.prototype).forEach(name => { + sourceCtors.forEach((sourceCtors) => { + Object.getOwnPropertyNames(sourceCtors.prototype).forEach((name) => { targetCtor.prototype[name] = sourceCtors.prototype[name]; }); }); @@ -246,16 +269,184 @@ export function isDuration(value: string): boolean { * @param {string} replaceValue The value to replace searchValue with in the value argument. * @returns {string | undefined} The value where each instance of searchValue was replaced with replacedValue. */ -export function replaceAll(value: string | undefined, searchValue: string, replaceValue: string): string | undefined { +export function replaceAll( + value: string | undefined, + searchValue: string, + replaceValue: string +): string | undefined { return !value || !searchValue ? value : value.split(searchValue).join(replaceValue || ""); } /** - * Determines whether the given enity is a basic/primitive type + * Determines whether the given entity is a basic/primitive type * (string, number, boolean, null, undefined). - * @param value Any entity - * @return boolean - true is it is primitive type, false otherwise. + * @param {any} value Any entity + * @return {boolean} - true is it is primitive type, false otherwise. */ export function isPrimitiveType(value: any): boolean { return (typeof value !== "object" && typeof value !== "function") || value === null; } + +/** + * Determines whether the given `value` is an empty string or not. + * @param {any} value Any entity + * @return {boolean} - true if it is equivalent to an empty string, false otherwise. + */ +export function stringIsEmpty(value: any) { + return isNull(value) || isUndefined(value) || value === ""; +} + +/** + * Checks if given `text` starts with the specified `prefix` + * @param text Input string + * @return {boolean} - true if yes, false otherwise. + */ +export function stringStartsWith(text: string, prefix: string) { + if (isNull(prefix)) { + return true; + } + + return text.substr(0, prefix.length) === prefix; +} + +/** + * Returns the number of keys (properties) in an object. + * + * @param {object} value The object which keys are to be counted. + * @return {number} The number of keys in the object. + */ +export function objectKeysLength(value: any) { + if (!value) { + return 0; + } + + return keys(value).length; +} + +/** + * Returns the name of the first property in an object. + * + * @param {object} value The object which key is to be returned. + * @return {number} The name of the first key in the object. + */ +export function objectFirstKey(value: any) { + if (value && Object.keys(value).length > 0) { + return Object.keys(value)[0]; + } + + // Object has no properties + return null; +} + +/** + * Determines whether the given `value` is a null object or not. + * @param {any} value Any entity + * @return {boolean} - true if yes, false otherwise. + */ +export function objectIsNull(value: any) { + return isNull(value) || isUndefined(value); +} + +/** + * Determines whether the given `value` is a `Date` object or not. + * @param {any} value Any entity + * @return {boolean} - true if yes, false otherwise. + */ +export function isDate(value: any) { + return Object.prototype.toString.call(value) == "[object Date]"; +} + +/** + * Determines whether the given `value` is a `string` object or not. + * @param {any} value Any entity + * @return {boolean} - true if yes, false otherwise. + */ +export function isString(value: any) { + return Object.prototype.toString.call(value) == "[object String]"; +} + +/** + * Determines whether the given `value` is a `Array` object or not. + * @param {any} value Any entity + * @return {boolean} - true if yes, false otherwise. + */ +export const isArray = + Array.isArray || + function(value: any) { + return Object.prototype.toString.call(value) == "[object Array]"; + }; + +/** + * Determines whether the given `value` is a `Object` or not. + * @param {any} value Any entity + * @return {boolean} - true if yes, false otherwise. + */ +export const isObject = function(value: any) { + return value === Object(value); +}; + +/** + * Determines whether the given `value` is an undefined entity or not. + * @param {any} value Any entity + * @return {boolean} - true if yes, false otherwise. + */ +export const isUndefined = function(value: any) { + return value === void 0; +}; + +/** + * Utility to iterate over given entity's values. + * @param {any} obj - The object to execute operation(s) on. + * @param {any} iterator - The iterator callback to use + * @param {any} context - Optional context for use with iterator + * @return {any} - the final extended object + */ +export const each = function(obj: any, iterator: any, context?: any) { + if (obj == null) return; + if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === {}) return; + } + } else { + for (var key in obj) { + if (has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === {}) return; + } + } + } +}; + +/** + * Extends given object `obj` with the passed in `source` object. + * @param {any} obj + * @param {any} source + * @return {any} - the final extended object + */ +export const extend = function(obj: any, source: any) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + return obj; +}; + +// Private helper utilities +const has = function(obj: any, key: any) { + return Object.prototype.hasOwnProperty.call(obj, key); +}; + +const isNull = function(value: any) { + return value === null; +}; + +const keys = + Object.keys || + function(obj: any) { + if (obj !== Object(obj)) throw new TypeError("Invalid object"); + var keys = []; + for (var key in obj) if (has(obj, key)) keys[keys.length] = key; + return keys; + }; diff --git a/sdk/core/core-http/lib/util/xml.browser.ts b/sdk/core/core-http/lib/util/xml.browser.ts index 059b329d141a..09d26b7e70fa 100644 --- a/sdk/core/core-http/lib/util/xml.browser.ts +++ b/sdk/core/core-http/lib/util/xml.browser.ts @@ -16,7 +16,8 @@ export function parseXML(str: string): Promise { let errorNS = ""; try { - errorNS = parser.parseFromString("INVALID", "text/xml").getElementsByTagName("parsererror")[0].namespaceURI!; + errorNS = parser.parseFromString("INVALID", "text/xml").getElementsByTagName("parsererror")[0] + .namespaceURI!; } catch (ignored) { // Most browsers will return a document containing , but IE will throw. } @@ -48,7 +49,12 @@ function domToObject(node: Node): any { const childNodeCount: number = node.childNodes.length; const firstChildNode: Node = node.childNodes[0]; - const onlyChildTextValue: string | undefined = (firstChildNode && childNodeCount === 1 && firstChildNode.nodeType === Node.TEXT_NODE && firstChildNode.nodeValue) || undefined; + const onlyChildTextValue: string | undefined = + (firstChildNode && + childNodeCount === 1 && + firstChildNode.nodeType === Node.TEXT_NODE && + firstChildNode.nodeValue) || + undefined; const elementWithAttributes: Element | undefined = asElementWithAttributes(node); if (elementWithAttributes) { @@ -93,12 +99,14 @@ const doc = document.implementation.createDocument(null, null, null); const serializer = new XMLSerializer(); export function stringifyXML(obj: any, opts?: { rootName?: string }) { - const rootName = opts && opts.rootName || "root"; + const rootName = (opts && opts.rootName) || "root"; const dom = buildNode(obj, rootName)[0]; - return '' + serializer.serializeToString(dom); + return ( + '' + serializer.serializeToString(dom) + ); } -function buildAttributes(attrs: { [key: string]: { toString(): string; } }): Attr[] { +function buildAttributes(attrs: { [key: string]: { toString(): string } }): Attr[] { const result = []; for (const key of Object.keys(attrs)) { const attr = doc.createAttribute(key); @@ -113,8 +121,7 @@ function buildNode(obj: any, elementName: string): Node[] { const elem = doc.createElement(elementName); elem.textContent = obj.toString(); return [elem]; - } - else if (Array.isArray(obj)) { + } else if (Array.isArray(obj)) { const result = []; for (const arrayElem of obj) { for (const child of buildNode(arrayElem, elementName)) { @@ -136,8 +143,15 @@ function buildNode(obj: any, elementName: string): Node[] { } } return [elem]; - } - else { + } else { throw new Error(`Illegal value passed to buildObject: ${obj}`); } } + +export function parseAtomXML(body: any): any { + throw new Error(`parseAtomXML(${body}) is not supported in browser yet.`); +} + +export function parseStringError(body: any): any { + throw new Error(`parseStringError(${body}) is not supported in browser yet.`); +} diff --git a/sdk/core/core-http/lib/util/xml.ts b/sdk/core/core-http/lib/util/xml.ts index 575d9ab00da3..e54a168f8203 100644 --- a/sdk/core/core-http/lib/util/xml.ts +++ b/sdk/core/core-http/lib/util/xml.ts @@ -35,3 +35,71 @@ export function parseXML(str: string): Promise { } }); } + +export function parseAtomXML(body: any): any { + var parsed; + var parser = new xml2js.Parser(_getDefaultSettingsForAtomXmlOperations()); + parser.parseString(_removeBOM(body.toString()), function(err: any, parsedBody: any) { + if (err) { + throw err; + } else { + parsed = parsedBody; + } + }); + + return parsed; +} + +export function parseStringError(body: any): any { + body = _removeBOM(body.toString()); + + var splitBody = body.split(/:|\r\n/g); + var locateSplitBody = splitBody.map(function(el: any) { + return el.toLowerCase(); + }); + + var codeIndex = locateSplitBody.indexOf("code"); + if (codeIndex !== -1) { + var resultObject: any = { + code: splitBody[codeIndex + 1].trim() + }; + + var detailIndex = locateSplitBody.indexOf("detail"); + if (detailIndex !== -1) { + resultObject.detail = splitBody[detailIndex + 1].trim(); + } + + return { error: resultObject }; + } else { + throw new Error("Invalid string error"); + } +} + +/** + * Gets the default xml2js settings applicable for Atom based XML operations. + * @ignore + * @return {object} The default settings + */ +function _getDefaultSettingsForAtomXmlOperations(): any { + var xml2jsSettings = xml2js.defaults["0.2"]; + xml2jsSettings.normalize = false; + xml2jsSettings.trim = false; + xml2jsSettings.attrkey = "$"; + xml2jsSettings.charkey = "_"; + xml2jsSettings.explicitArray = false; + xml2jsSettings.ignoreAttrs = true; + + return xml2jsSettings; +} + +/** + * + * @param str Helper utility to clean up unintended characters that get appended by OS. + */ +function _removeBOM(str: any) { + if (str.charCodeAt(0) === 0xfeff || str.charCodeAt(0) === 0xffef) { + str = str.substring(1); + } + + return str; +} diff --git a/sdk/core/core-http/lib/webResource.ts b/sdk/core/core-http/lib/webResource.ts index cac837cae01c..c01ff5efba28 100644 --- a/sdk/core/core-http/lib/webResource.ts +++ b/sdk/core/core-http/lib/webResource.ts @@ -9,9 +9,23 @@ import { HttpOperationResponse } from "./httpOperationResponse"; import { OperationResponse } from "./operationResponse"; import { ProxySettings } from "./serviceClient"; import { AbortSignalLike } from "@azure/abort-controller"; - -export type HttpMethods = "GET" | "PUT" | "POST" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" | "TRACE"; -export type HttpRequestBody = Blob | string | ArrayBuffer | ArrayBufferView | (() => NodeJS.ReadableStream); +import { AtomXmlOperationSpec } from "./atomXmlOperationSpec"; + +export type HttpMethods = + | "GET" + | "PUT" + | "POST" + | "DELETE" + | "PATCH" + | "HEAD" + | "OPTIONS" + | "TRACE"; +export type HttpRequestBody = + | Blob + | string + | ArrayBuffer + | ArrayBufferView + | (() => NodeJS.ReadableStream); /** * Fired in response to upload or download progress. @@ -20,7 +34,7 @@ export type TransferProgressEvent = { /** * The number of bytes loaded so far. */ - loadedBytes: number + loadedBytes: number; }; /** @@ -50,10 +64,14 @@ export class WebResource { * HttpOperationResponse combination. If this is undefined, then a simple status code lookup will * be used. */ - operationResponseGetter?: (operationSpec: OperationSpec, response: HttpOperationResponse) => (undefined | OperationResponse); + operationResponseGetter?: ( + operationSpec: OperationSpec, + response: HttpOperationResponse + ) => undefined | OperationResponse; formData?: any; - query?: { [key: string]: any; }; + query?: { [key: string]: any }; operationSpec?: OperationSpec; + atomXmlOperationSpec?: AtomXmlOperationSpec; withCredentials: boolean; timeout: number; proxySettings?: ProxySettings; @@ -71,8 +89,8 @@ export class WebResource { url?: string, method?: HttpMethods, body?: any, - query?: { [key: string]: any; }, - headers?: { [key: string]: any; } | HttpHeaders, + query?: { [key: string]: any }, + headers?: { [key: string]: any } | HttpHeaders, streamResponseBody?: boolean, withCredentials?: boolean, abortSignal?: AbortSignalLike, @@ -80,12 +98,12 @@ export class WebResource { onUploadProgress?: (progress: TransferProgressEvent) => void, onDownloadProgress?: (progress: TransferProgressEvent) => void, proxySettings?: ProxySettings, - keepAlive?: boolean) { - + keepAlive?: boolean + ) { this.streamResponseBody = streamResponseBody; this.url = url || ""; this.method = method || "GET"; - this.headers = (headers instanceof HttpHeaders ? headers : new HttpHeaders(headers)); + this.headers = headers instanceof HttpHeaders ? headers : new HttpHeaders(headers); this.body = body; this.query = query; this.formData = undefined; @@ -127,18 +145,22 @@ export class WebResource { } if (options.url && options.pathTemplate) { - throw new Error("options.url and options.pathTemplate are mutually exclusive. Please provide exactly one of them."); + throw new Error( + "options.url and options.pathTemplate are mutually exclusive. Please provide exactly one of them." + ); } - - if ((options.pathTemplate == undefined || typeof options.pathTemplate.valueOf() !== "string") && (options.url == undefined || typeof options.url.valueOf() !== "string")) { + if ( + (options.pathTemplate == undefined || typeof options.pathTemplate.valueOf() !== "string") && + (options.url == undefined || typeof options.url.valueOf() !== "string") + ) { throw new Error("Please provide exactly one of options.pathTemplate or options.url."); } // 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\"."); + throw new Error('options.url must be of type "string".'); } this.url = options.url; } @@ -147,35 +169,55 @@ export class WebResource { if (options.method) { const 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)); + throw new Error( + 'The provided method "' + + options.method + + '" is invalid. Supported HTTP methods are: ' + + JSON.stringify(validMethods) + ); } } - this.method = (options.method.toUpperCase() as HttpMethods); + this.method = options.method.toUpperCase() as HttpMethods; // construct the url if path template is provided if (options.pathTemplate) { const { pathTemplate, pathParameters } = options; if (typeof pathTemplate !== "string") { - throw new Error("options.pathTemplate must be of type \"string\"."); + throw new Error('options.pathTemplate must be of type "string".'); } if (!options.baseUrl) { options.baseUrl = "https://management.azure.com"; } const baseUrl = options.baseUrl; - let url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + (pathTemplate.startsWith("/") ? pathTemplate.slice(1) : pathTemplate); - const segments = url.match(/({\w*\s*\w*})/ig); + let url = + baseUrl + + (baseUrl.endsWith("/") ? "" : "/") + + (pathTemplate.startsWith("/") ? pathTemplate.slice(1) : pathTemplate); + const segments = url.match(/({\w*\s*\w*})/gi); if (segments && segments.length) { if (!pathParameters) { - throw new Error(`pathTemplate: ${pathTemplate} has been provided. Hence, options.pathParameters must also be provided.`); + throw new Error( + `pathTemplate: ${pathTemplate} has been provided. Hence, options.pathParameters must also be provided.` + ); } - segments.forEach(function (item) { + segments.forEach(function(item) { const pathParamName = item.slice(1, -1); const pathParam = (pathParameters as { [key: string]: any })[pathParamName]; - if (pathParam === null || pathParam === undefined || !(typeof pathParam === "string" || typeof pathParam === "object")) { - throw new Error(`pathTemplate: ${pathTemplate} contains the path parameter ${pathParamName}` + - ` however, it is not present in ${pathParameters} - ${JSON.stringify(pathParameters, undefined, 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 ( + pathParam === null || + pathParam === undefined || + !(typeof pathParam === "string" || typeof pathParam === "object") + ) { + throw new Error( + `pathTemplate: ${pathTemplate} contains the path parameter ${pathParamName}` + + ` however, it is not present in ${pathParameters} - ${JSON.stringify( + pathParameters, + undefined, + 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") { @@ -184,7 +226,9 @@ export class WebResource { 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.`); + 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); @@ -201,9 +245,11 @@ export class WebResource { if (options.queryParameters) { const queryParameters = options.queryParameters; if (typeof 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 }.`); + 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) { @@ -219,10 +265,11 @@ export class WebResource { if (typeof queryParam === "string") { queryParams.push(queryParamName + "=" + encodeURIComponent(queryParam)); this.query[queryParamName] = encodeURIComponent(queryParam); - } - else if (typeof queryParam === "object") { + } 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.`); + 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); @@ -233,7 +280,7 @@ export class WebResource { } } } - }// end-of-for + } // end-of-for // append the queryString this.url += queryParams.join("&"); } @@ -272,7 +319,11 @@ export class WebResource { } } else { if (options.serializationMapper) { - this.body = new Serializer(options.mappers).serialize(options.serializationMapper, options.body, "requestBody"); + this.body = new Serializer(options.mappers).serialize( + options.serializationMapper, + options.body, + "requestBody" + ); } if (!options.disableJsonStringifyOnBody) { this.body = JSON.stringify(options.body); @@ -303,7 +354,8 @@ export class WebResource { this.abortSignal, this.timeout, this.onUploadProgress, - this.onDownloadProgress); + this.onDownloadProgress + ); if (this.formData) { result.formData = this.formData; @@ -313,6 +365,10 @@ export class WebResource { result.operationSpec = this.operationSpec; } + if (this.atomXmlOperationSpec) { + result.atomXmlOperationSpec = this.atomXmlOperationSpec; + } + if (this.shouldDeserialize) { result.shouldDeserialize = this.shouldDeserialize; } diff --git a/sdk/core/core-http/package.json b/sdk/core/core-http/package.json index 790f7301dc31..5955a1d9b520 100644 --- a/sdk/core/core-http/package.json +++ b/sdk/core/core-http/package.json @@ -114,6 +114,7 @@ "@azure/core-auth": "1.0.0-preview.3", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.1", + "buffer": "^5.2.1", "form-data": "^2.5.0", "node-fetch": "^2.6.0", "process": "^0.11.10", @@ -121,7 +122,8 @@ "tslib": "^1.9.3", "tunnel": "^0.0.6", "uuid": "^3.3.2", - "xml2js": "^0.4.19" + "xml2js": "^0.4.19", + "xmlbuilder": "^0.4.3" }, "devDependencies": { "@azure/logger-js": "^1.0.2", diff --git a/sdk/servicebus/service-bus/package.json b/sdk/servicebus/service-bus/package.json index 2c58bd1d8ea0..d2338d34a5c3 100644 --- a/sdk/servicebus/service-bus/package.json +++ b/sdk/servicebus/service-bus/package.json @@ -66,6 +66,7 @@ "sideEffects": false, "dependencies": { "@azure/amqp-common": "1.0.0-preview.6", + "@azure/core-http": "1.0.0-preview.3", "@azure/ms-rest-nodeauth": "^0.9.2", "@types/is-buffer": "^2.0.0", "@types/long": "^4.0.0", diff --git a/sdk/servicebus/service-bus/review/service-bus.api.md b/sdk/servicebus/service-bus/review/service-bus.api.md index 1792bcd00e56..1a6b67998c2b 100644 --- a/sdk/servicebus/service-bus/review/service-bus.api.md +++ b/sdk/servicebus/service-bus/review/service-bus.api.md @@ -10,12 +10,19 @@ import { DataTransformer } from '@azure/amqp-common'; import { delay } from '@azure/amqp-common'; import { Delivery } from 'rhea-promise'; import { DeviceTokenCredentials } from '@azure/ms-rest-nodeauth'; +import { HttpOperationResponse } from '@azure/core-http'; +import { logPolicy } from '@azure/core-http'; import Long from 'long'; import { MessagingError } from '@azure/amqp-common'; import { MSITokenCredentials } from '@azure/ms-rest-nodeauth'; +import { proxyPolicy } from '@azure/core-http'; +import { ResourceSerializer } from '@azure/core-http'; +import { ServiceClient } from '@azure/core-http'; +import { ServiceClientOptions } from '@azure/core-http'; import { TokenInfo } from '@azure/amqp-common'; import { TokenProvider } from '@azure/amqp-common'; import { TokenType } from '@azure/amqp-common'; +import { userAgentPolicy } from '@azure/core-http'; import { UserTokenCredentials } from '@azure/ms-rest-nodeauth'; import { WebSocketImpl } from 'rhea-promise'; @@ -44,6 +51,10 @@ export { delay } export { Delivery } +export { HttpOperationResponse } + +export { logPolicy } + // @public export interface MessageHandlerOptions { autoComplete?: boolean; @@ -63,13 +74,15 @@ export interface OnMessage { (message: ServiceBusMessage): Promise; } +export { proxyPolicy } + // Warning: (ae-forgotten-export) The symbol "Client" needs to be exported by the entry point index.d.ts // // @public export class QueueClient implements Client { close(): Promise; - createReceiver(receiveMode: ReceiveMode): Receiver; createReceiver(receiveMode: ReceiveMode, sessionOptions: SessionReceiverOptions): SessionReceiver; + createReceiver(receiveMode: ReceiveMode): Receiver; createSender(): Sender; readonly entityPath: string; static getDeadLetterQueuePath(queueName: string): string; @@ -150,6 +163,39 @@ export class Sender { sendBatch(messages: SendableMessageInfo[]): Promise; } +// @public +export class ServiceBusAtomManagementClient extends ServiceClient { + constructor(connectionString: any, options?: ServiceClientOptions); + createQueue(queuePath: string, options: any): Promise; + createRule(topicPath: string, subscriptionPath: string, rule: string, options: any): Promise; + createSubscription(topicPath: string, subscriptionPath: string, options: any): Promise; + createTopic(topicPath: string, options: any): Promise; + deleteQueue(queuePath: any): any; + deleteRule(topicPath: string, subscriptionPath: string, rule: string): any; + deleteSubscription(topicPath: string, subscriptionPath: string): any; + deleteTopic(topicPath: any): any; + // (undocumented) + endpoint: any; + formatDeadLetterPath(queuePath: any): any; + getQueue(queuePath: string): Promise; + getRule(topicPath: string, subscriptionPath: string, rule: string): Promise; + getSubscription(topicPath: string, subscriptionPath: string): Promise; + getTopic(topicPath: string): Promise; + listQueues(options?: any): Promise; + listRules(topicPath: string, subscriptionPath: string, options?: any): Promise; + listSubscriptions(topicPath: string, options?: any): Promise; + listTopics(options?: any): Promise; + // (undocumented) + queueResourceSerializer: ResourceSerializer; + // (undocumented) + ruleResourceSerializer: ResourceSerializer; + // (undocumented) + subscriptionResourceSerializer: ResourceSerializer; + // (undocumented) + topicResourceSerializer: ResourceSerializer; + updateQueue(queuePath: any, options: any): Promise; +} + // @public export class ServiceBusClient { close(): Promise; @@ -211,6 +257,8 @@ export class ServiceBusMessage implements ReceivedMessage { viaPartitionKey?: string; } +export { ServiceClientOptions } + // @public export interface SessionMessageHandlerOptions { autoComplete?: boolean; @@ -275,6 +323,8 @@ export class TopicClient implements Client { readonly id: string; } +export { userAgentPolicy } + export { WebSocketImpl } diff --git a/sdk/servicebus/service-bus/rollup.config.js b/sdk/servicebus/service-bus/rollup.config.js index 0561cc0a3c26..9ba51883df75 100644 --- a/sdk/servicebus/service-bus/rollup.config.js +++ b/sdk/servicebus/service-bus/rollup.config.js @@ -9,13 +9,13 @@ if (!process.env.ONLY_BROWSER) { inputs.push(base.nodeConfig()); } -if (!process.env.ONLY_NODE) { +/* if (!process.env.ONLY_NODE) { inputs.push(base.browserConfig()); inputs.push(base.browserConfig({ production: true })); } if (process.env.BROWSER_TEST) { inputs.push(base.browserConfig({ test: true })); -} +} */ export default inputs; diff --git a/sdk/servicebus/service-bus/samples/typescript/gettingStarted/atomManagementApi.ts b/sdk/servicebus/service-bus/samples/typescript/gettingStarted/atomManagementApi.ts new file mode 100644 index 000000000000..0b385c640ad8 --- /dev/null +++ b/sdk/servicebus/service-bus/samples/typescript/gettingStarted/atomManagementApi.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +"use strict"; + +import { + ServiceBusAtomManagementClient, + ServiceClientOptions, + logPolicy, + HttpOperationResponse +} from "@azure/service-bus"; + +async function main(): Promise { + const clientOptions: ServiceClientOptions = { + requestPolicyFactories: [logPolicy()] + }; + + const serviceBusAtomManagementClient: ServiceBusAtomManagementClient = new ServiceBusAtomManagementClient( + "", + clientOptions + ); + + const response: HttpOperationResponse = await serviceBusAtomManagementClient.createQueue( + "testQueuePath4", + { + LockDuration: "PT1M", + MaxSizeInMegabytes: "1024", + RequiresDuplicateDetection: "false", + RequiresSession: "false", + DeadLetteringOnMessageExpiration: "false", + MaxDeliveryCount: "10", + EnableBatchedOperations: "true", + EnablePartitioning: "false" + } + ); + + console.log(JSON.stringify(response.parsedBody, undefined, 2)); +} + +main().catch((err) => { + console.log("Error occurred: ", err); +}); diff --git a/sdk/servicebus/service-bus/src/index.ts b/sdk/servicebus/service-bus/src/index.ts index 0859833dbeaf..3df0477492cb 100644 --- a/sdk/servicebus/service-bus/src/index.ts +++ b/sdk/servicebus/service-bus/src/index.ts @@ -35,3 +35,12 @@ export { ReceiveMode } from "./serviceBusMessage"; export { Delivery, WebSocketImpl } from "rhea-promise"; + +export { ServiceBusAtomManagementClient } from "./serviceBusAtomManagementClient"; +export { + HttpOperationResponse, + ServiceClientOptions, + userAgentPolicy, + logPolicy, + proxyPolicy +} from "@azure/core-http"; diff --git a/sdk/servicebus/service-bus/src/serializers/queueResourceSerializer.ts b/sdk/servicebus/service-bus/src/serializers/queueResourceSerializer.ts new file mode 100644 index 000000000000..23508977e225 --- /dev/null +++ b/sdk/servicebus/service-bus/src/serializers/queueResourceSerializer.ts @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { AtomResourceSerializerBase, ResourceSerializer } from "@azure/core-http"; +import { ServiceBusAtomXmlConstants } from "../util/constants"; + +export class QueueResourceSerializer extends ResourceSerializer { + serialize(resource: any): any { + var properties = [ + ServiceBusAtomXmlConstants.LOCK_DURATION, + ServiceBusAtomXmlConstants.MAX_SIZE_IN_MEGABYTES, + ServiceBusAtomXmlConstants.REQUIRES_DUPLICATE_DETECTION, + ServiceBusAtomXmlConstants.REQUIRES_SESSION, + ServiceBusAtomXmlConstants.DEFAULT_MESSAGE_TIME_TO_LIVE, + ServiceBusAtomXmlConstants.DEAD_LETTERING_ON_MESSAGE_EXPIRATION, + ServiceBusAtomXmlConstants.DUPLICATE_DETECTION_HISTORY_TIME_WINDOW, + ServiceBusAtomXmlConstants.MAX_DELIVERY_COUNT, + ServiceBusAtomXmlConstants.ENABLE_BATCHED_OPERATIONS, + ServiceBusAtomXmlConstants.SIZE_IN_BYTES, + ServiceBusAtomXmlConstants.MESSAGE_COUNT, + ServiceBusAtomXmlConstants.ENABLE_PARTITIONING + ]; + + return AtomResourceSerializerBase._serialize("QueueDescription", resource, properties); + } + + parse(xml: any): any { + return AtomResourceSerializerBase._parse(["QueueName"], xml); + } +} diff --git a/sdk/servicebus/service-bus/src/serializers/ruleResourceSerializer.ts b/sdk/servicebus/service-bus/src/serializers/ruleResourceSerializer.ts new file mode 100644 index 000000000000..a629b24c5b03 --- /dev/null +++ b/sdk/servicebus/service-bus/src/serializers/ruleResourceSerializer.ts @@ -0,0 +1,103 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { AtomResourceSerializerBase, ResourceSerializer } from "@azure/core-http"; + +export class RuleResourceSerializer extends ResourceSerializer { + serialize(rule: any): any { + var properties = ["Filter", "Action"]; + + var resource: any = {}; + + if (rule) { + var filters = []; + if (rule.sqlExpressionFilter) { + var sqlFilter = { + $: { + "i:type": "SqlFilter" + }, + SqlExpression: rule.sqlExpressionFilter, + CompatibilityLevel: 20 + }; + + filters.push(sqlFilter); + } else if (rule.correlationIdFilter) { + var correlationFilter = { + $: { + "i:type": "CorrelationFilter" + }, + CorrelationId: rule.correlationIdFilter + }; + + filters.push(correlationFilter); + } else if (rule.trueFilter) { + var trueFilter = { + $: { + "i:type": "TrueFilter" + }, + SqlExpression: rule.trueFilter, + CompatibilityLevel: 20 + }; + + filters.push(trueFilter); + } else if (rule.falseFilter) { + var falseFilter = { + $: { + "i:type": "FalseFilter" + }, + SqlExpression: rule.falseFilter, + CompatibilityLevel: 20 + }; + + filters.push(falseFilter); + } + + if (filters.length > 0) { + resource.Filter = filters; + } + + var actions = []; + + if (rule.sqlRuleAction) { + var sqlAction = { + $: { + "i:type": "SqlFilterExpression" + }, + SqlExpression: rule.sqlRuleAction + }; + + actions.push(sqlAction); + } else { + var emptyRuleAction = { + $: { + "i:type": "EmptyRuleAction" + } + }; + + actions.push(emptyRuleAction); + } + + if (actions.length > 0) { + resource.Action = actions; + } + } + return AtomResourceSerializerBase._serialize("RuleDescription", resource, properties); + } + + parse(xml: any): any { + return AtomResourceSerializerBase._parse(["TopicName", "SubscriptionName", "RuleName"], xml); + } +} diff --git a/sdk/servicebus/service-bus/src/serializers/subscriptionResourceSerializer.ts b/sdk/servicebus/service-bus/src/serializers/subscriptionResourceSerializer.ts new file mode 100644 index 000000000000..03dee7fcc43c --- /dev/null +++ b/sdk/servicebus/service-bus/src/serializers/subscriptionResourceSerializer.ts @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { AtomResourceSerializerBase, ResourceSerializer } from "@azure/core-http"; +import { ServiceBusAtomXmlConstants } from "../util/constants"; + +export class SubscriptionResourceSerializer extends ResourceSerializer { + serialize(resource: any): any { + var properties = [ + ServiceBusAtomXmlConstants.LOCK_DURATION, + ServiceBusAtomXmlConstants.REQUIRES_SESSION, + ServiceBusAtomXmlConstants.DEFAULT_MESSAGE_TIME_TO_LIVE, + ServiceBusAtomXmlConstants.DEAD_LETTERING_ON_MESSAGE_EXPIRATION, + ServiceBusAtomXmlConstants.DEAD_LETTERING_ON_FILTER_EVALUATION_EXCEPTIONS, + ServiceBusAtomXmlConstants.MESSAGE_COUNT, + ServiceBusAtomXmlConstants.MAX_DELIVERY_COUNT, + ServiceBusAtomXmlConstants.ENABLE_BATCHED_OPERATIONS, + ServiceBusAtomXmlConstants.AUTO_DELETE_ON_IDLE + ]; + + return AtomResourceSerializerBase._serialize("SubscriptionDescription", resource, properties); + } + + parse(xml: any): any { + return AtomResourceSerializerBase._parse(["TopicName", "SubscriptionName"], xml); + } +} diff --git a/sdk/servicebus/service-bus/src/serializers/topicResourceSerializer.ts b/sdk/servicebus/service-bus/src/serializers/topicResourceSerializer.ts new file mode 100644 index 000000000000..be54e434ae30 --- /dev/null +++ b/sdk/servicebus/service-bus/src/serializers/topicResourceSerializer.ts @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { AtomResourceSerializerBase, ResourceSerializer } from "@azure/core-http"; +import { ServiceBusAtomXmlConstants } from "../util/constants"; + +export class TopicResourceSerializer extends ResourceSerializer { + serialize(resource: any): any { + var properties = [ + ServiceBusAtomXmlConstants.DEFAULT_MESSAGE_TIME_TO_LIVE, + ServiceBusAtomXmlConstants.MAX_SIZE_IN_MEGABYTES, + ServiceBusAtomXmlConstants.REQUIRES_DUPLICATE_DETECTION, + ServiceBusAtomXmlConstants.DEFAULT_MESSAGE_TIME_TO_LIVE, + ServiceBusAtomXmlConstants.DUPLICATE_DETECTION_HISTORY_TIME_WINDOW, + ServiceBusAtomXmlConstants.ENABLE_BATCHED_OPERATIONS, + ServiceBusAtomXmlConstants.SIZE_IN_BYTES, + ServiceBusAtomXmlConstants.SUPPORT_ORDERING, + ServiceBusAtomXmlConstants.ENABLE_PARTITIONING + ]; + + return AtomResourceSerializerBase._serialize("TopicDescription", resource, properties); + } + + parse(xml: any): any { + return AtomResourceSerializerBase._parse(["TopicName"], xml); + } +} diff --git a/sdk/servicebus/service-bus/src/serviceBusAtomManagementClient.ts b/sdk/servicebus/service-bus/src/serviceBusAtomManagementClient.ts new file mode 100644 index 000000000000..9f8920c8a196 --- /dev/null +++ b/sdk/servicebus/service-bus/src/serviceBusAtomManagementClient.ts @@ -0,0 +1,541 @@ +// +// Copyright (c) Microsoft and contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + WebResource, + ServiceClient, + ServiceClientOptions, + HttpOperationResponse, + ServiceBusSASServiceClientCredentials, + AtomXmlOperationSpec, + signingPolicy, + userAgentPolicy, + serviceBusAtomSerializationPolicy, + RequestPolicyFactory, + ResourceSerializer, + URLBuilder +} from "@azure/core-http"; + +import { QueueResourceSerializer } from "./serializers/queueResourceSerializer"; +import { TopicResourceSerializer } from "./serializers/topicResourceSerializer"; +import { SubscriptionResourceSerializer } from "./serializers/subscriptionResourceSerializer"; +import { RuleResourceSerializer } from "./serializers/ruleResourceSerializer"; + +/** + * All operations return a `Promise, + * result: , + * error: + * } + */ +export class ServiceBusAtomManagementClient extends ServiceClient { + endpoint: any; + + queueResourceSerializer: ResourceSerializer; + topicResourceSerializer: ResourceSerializer; + subscriptionResourceSerializer: ResourceSerializer; + ruleResourceSerializer: ResourceSerializer; + + /** + * Initializes a new instance of the ServiceBusManagementClient class. + * @param credentials Credentials needed for the client to connect to Azure. + * @param options The parameter options + */ + constructor(connectionString: any, options?: ServiceClientOptions) { + const connectionStringObj = ServiceBusAtomManagementClient.parseConnectionString( + connectionString + ); + const credentials = new ServiceBusSASServiceClientCredentials( + connectionStringObj.SharedAccessKeyName, + connectionStringObj.SharedAccessKey + ); + + if (!options) { + options = {}; + } + const requestPolicyFactories = (options.requestPolicyFactories as RequestPolicyFactory[]) || []; + requestPolicyFactories.push(userAgentPolicy()); + requestPolicyFactories.push(serviceBusAtomSerializationPolicy()); + requestPolicyFactories.push(signingPolicy(credentials)); + options!.requestPolicyFactories = requestPolicyFactories; + options = { + requestPolicyFactories: requestPolicyFactories + }; + + super(credentials, options); + this.queueResourceSerializer = new QueueResourceSerializer(); + this.topicResourceSerializer = new TopicResourceSerializer(); + this.subscriptionResourceSerializer = new SubscriptionResourceSerializer(); + this.ruleResourceSerializer = new RuleResourceSerializer(); + this.endpoint = (connectionString.match("Endpoint=sb://(.*).servicebus.windows.net") || "")[1]; + } + + /** + * Creates a queue. + * + * @param {string} queuePath A string object that represents the name of the queue to create. + * @param {object} [options] The request options. + * @param {int} [options.MaxSizeInMegaBytes] Specifies the maximum queue size in megabytes. Any attempt to enqueue a message that will cause the queue to exceed this value will fail. + * @param {PTnHnMnS} [options.DefaultMessageTimeToLive] Depending on whether DeadLettering is enabled, a message is automatically moved to the DeadLetterQueue or deleted if it has been stored in the queue for longer than the specified time. This value is overwritten by a TTL specified on the message if and only if the message TTL is smaller than the TTL set on the queue. This value is immutable after the Queue has been created. + * @param {PTnHnMnS} [options.LockDuration] Determines the amount of time in seconds in which a message should be locked for processing by a receiver. After this period, the message is unlocked and available for consumption by the next receiver. Settable only at queue creation time. + * @param {bool} [options.RequiresSession] Settable only at queue creation time. If set to true, the queue will be session-aware and only SessionReceiver will be supported. Session-aware queues are not supported through REST. + * @param {bool} [options.RequiresDuplicateDetection] Settable only at queue creation time. + * @param {bool} [options.DeadLetteringOnMessageExpiration] This field controls how the Service Bus handles a message whose TTL has expired. If it is enabled and a message expires, the Service Bus moves the message from the queue into the queue’s dead-letter sub-queue. If disabled, message will be permanently deleted from the queue. Settable only at queue creation time. + * @param {bool} [options.DuplicateDetectionHistoryTimeWindow] Specifies the time span during which the Service Bus detects message duplication. + * @param {bool} [options.EnablePartitioning] Specifies whether the queue should be partitioned. + * `error` will contain information + * if an error occurs; otherwise `createqueueresult` will contain + * the new queue information. + * `response` will contain information related to this operation. + * @return {Promise { + return this._createResource(queuePath, options, false, this.queueResourceSerializer); + } + + /** + * Retrieves a queue. + * + * @param {string} queuePath A string object that represents the name of the queue to retrieve. + * @return {Promise { + return this._getResource(queuePath, this.queueResourceSerializer); + } + + /** + * Returns a list of queues. + * + * @param {object} [options] The request options. + * @param {int} [options.top] The top clause for listing queues. + * @param {int} [options.skip] The skip clause for listing queues. + * @return {Promise { + return this._listResources("$Resources/Queues", options, this.queueResourceSerializer); + } + + /** + * Creates a queue. + * + * @param {string} queuePath A string object that represents the name of the queue to update. + * @param {object} [options] The request options. + * @param {int} [options.MaxSizeInMegaBytes] Specifies the maximum queue size in megabytes. Any attempt to enqueue a message that will cause the queue to exceed this value will fail. + * @param {PTnHnMnS} [options.DefaultMessageTimeToLive] Depending on whether DeadLettering is enabled, a message is automatically moved to the DeadLetterQueue or deleted if it has been stored in the queue for longer than the specified time. This value is overwritten by a TTL specified on the message if and only if the message TTL is smaller than the TTL set on the queue. This value is immutable after the Queue has been created. + * @param {PTnHnMnS} [options.LockDuration] Determines the amount of time in seconds in which a message should be locked for processing by a receiver. After this period, the message is unlocked and available for consumption by the next receiver. Settable only at queue creation time. + * @param {bool} [options.RequiresSession] Settable only at queue creation time. If set to true, the queue will be session-aware and only SessionReceiver will be supported. Session-aware queues are not supported through REST. + * @param {bool} [options.RequiresDuplicateDetection] Settable only at queue creation time. + * @param {bool} [options.DeadLetteringOnMessageExpiration] This field controls how the Service Bus handles a message whose TTL has expired. If it is enabled and a message expires, the Service Bus moves the message from the queue into the queue’s dead-letter sub-queue. If disabled, message will be permanently deleted from the queue. Settable only at queue creation time. + * @param {bool} [options.DuplicateDetectionHistoryTimeWindow] Specifies the time span during which the Service Bus detects message duplication. + * @param {bool} [options.EnablePartitioning] Specifies whether the queue should be partitioned. + * @return {Promise { + return this._createResource(queuePath, options, true, this.queueResourceSerializer); + } + + /** + * Deletes a queue. + * + * @param {string} queuePath A string object that represents the name of the queue to delete. + * @return {Promise { + return this._createResource(topicPath, options, false, this.topicResourceSerializer); + } + + /** + * Deletes a topic. + * + * @param {string} topicPath A String object that represents the name of the queue to delete. + * @return {PromiseString object that represents the name of the topic to retrieve. + * @return {Promise { + return this._getResource(topicPath, this.topicResourceSerializer); + } + + /** + * Returns a list of topics. + * + * @param {object} [options] The request options. + * @param {int} [options.top] The number of topics to fetch. + * @param {int} [options.skip] The number of topics to skip. + * @return {Promise { + return this._listResources("$Resources/Topics", options, this.topicResourceSerializer); + } + + /** + * Creates a subscription. + * + * @param {string} topicPath A string object that represents the name of the topic for the subscription. + * @param {string} subscriptionPath A string object that represents the name of the subscription. + * @param {object} [options] The request options. + * @param {PTnHnMnS} [options.LockDuration] The default lock duration is applied to subscriptions that do not define a lock duration. Settable only at subscription creation time. + * @param {bool} [options.RequiresSession] Settable only at subscription creation time. If set to true, the subscription will be session-aware and only SessionReceiver will be supported. Session-aware subscription are not supported through REST. + * @param {PTnHnMnS} [options.DefaultMessageTimeToLive] Determines how long a message lives in the subscription. Based on whether dead-lettering is enabled, a message whose TTL has expired will either be moved to the subscription’s associated DeadLtterQueue or permanently deleted. + * @param {bool} [options.EnableDeadLetteringOnMessageExpiration] This field controls how the Service Bus handles a message whose TTL has expired. If it is enabled and a message expires, the Service Bus moves the message from the queue into the subscription’s dead-letter sub-queue. If disabled, message will be permanently deleted from the subscription’s main queue. Settable only at subscription creation time. + * @param {bool} [options.EnableDeadLetteringOnFilterEvaluationExceptions] Determines how the Service Bus handles a message that causes an exception during a subscription’s filter evaluation. If the value is set to true, the message that caused the exception will be moved to the subscription’s dead-letter queue. Otherwise, it will be discarded. By default this parameter is set to true, allowing the user a chance to investigate the cause of the exception. It can occur from a malformed message or some incorrect assumptions being made in the filter about the form of the message. Settable only at topic creation time. + * @return {Promise { + const fullPath = ServiceBusAtomManagementClient.getSubscriptionPath( + topicPath, + subscriptionPath + ); + return this._createResource(fullPath, options, false, this.subscriptionResourceSerializer); + } + + /** + * Deletes a subscription. + * + * @param {string} topicPath A string object that represents the name of the topic for the subscription. + * @param {string} subscriptionPath A string object that represents the name of the subscription to delete. + * @return {Promise { + const fullPath = ServiceBusAtomManagementClient.getSubscriptionPath( + topicPath, + subscriptionPath + ); + return this._getResource(fullPath, this.subscriptionResourceSerializer); + } + + /** + * Returns a list of subscriptions. + * + * @param {string} topicPath A string object that represents the name of the topic for the subscriptions to retrieve. + * @param {object} [options] The request options. + * @param {int} [options.top] The number of topics to fetch. + * @param {int} [options.skip] The number of topics to skip. + * @return {Promise { + return this._listResources( + topicPath + "/Subscriptions/", + options, + this.subscriptionResourceSerializer + ); + } + + /** + * Creates a rule. + * + * @param {string} topicPath A string object that represents the name of the topic for the subscription. + * @param {string} subscriptionPath A string object that represents the name of the subscription for which the rule will be created. + * @param {string} rulePath A string object that represents the name of the rule to be created. + * @param {object} [options] The request options. + * @param {string} [options.trueFilter] Defines the expression that the rule evaluates as a true filter. + * @param {string} [options.falseFilter] Defines the expression that the rule evaluates as a false filter. + * @param {string} [options.sqlExpressionFilter] Defines the expression that the rule evaluates. The expression string is interpreted as a SQL92 expression which must evaluate to True or False. Only one between a correlation and a sql expression can be defined. + * @param {string} [options.correlationIdFilter] Defines the expression that the rule evaluates. Only the messages whose CorrelationId match the CorrelationId set in the filter expression are allowed. Only one between a correlation and a sql expression can be defined. + * @param {string} [options.sqlRuleAction] Defines the expression that the rule evaluates. If the rule is of type SQL, the expression string is interpreted as a SQL92 expression which must evaluate to True or False. If the rule is of type CorrelationFilterExpression then only the messages whose CorrelationId match the CorrelationId set in the filter expression are allowed. + * @return {Promise { + const fullPath = ServiceBusAtomManagementClient.getRulePath(topicPath, subscriptionPath, rule); + + return this._createResource(fullPath, options, false, this.ruleResourceSerializer); + } + + /** + * Deletes a rule. + * + * @param {string} topicPath A string object that represents the name of the topic for the subscription. + * @param {string} subscriptionPath A string object that represents the name of the subscription for which the rule will be deleted. + * @param {string} rulePath A string object that represents the name of the rule to delete. + * @return {Promise { + const fullPath = ServiceBusAtomManagementClient.getRulePath(topicPath, subscriptionPath, rule); + return this._getResource(fullPath, this.ruleResourceSerializer); + } + + /** + * Returns a list of rules. + * + * @param {string} topicPath A string object that represents the name of the topic for the subscription. + * @param {string} subscriptionPath A string object that represents the name of the subscription whose rules are being retrieved. + * @param {object} [options] The request options. + * @param {int} [options.top] The number of topics to fetch. + * @param {int} [options.skip] The number of topics to skip. + * @return {Promise { + const fullPath = + ServiceBusAtomManagementClient.getSubscriptionPath(topicPath, subscriptionPath) + "/Rules/"; + return this._listResources(fullPath, options, this.ruleResourceSerializer); + } + + /** + * Formats a queue path to point towards its dead letter queue. + * + * @param {string} queuePath A string object that represents the name of the queue whose dead letter path you want. + * @return {string} + * The path to the queue's dead letter queue + */ + formatDeadLetterPath(queuePath: any): any { + return `${queuePath}/$DeadLetterQueue`; + } + + /** + * Creates a resource. + * + * @param {string} path The resource path. + * @param {object} requestBody The resource handler. + * @param {boolean} isUpdate `isUpdate` flag indicates whether the `PUT` request + * must attempt to update the resource or to create new one. + * @param {ResourceSerializer} serializer The XML serializer to use. + * @return {Promise} + */ + private async _createResource( + path: string, + requestBody: any, + isUpdate: boolean = false, + serializer: ResourceSerializer + ): Promise { + let webResource: WebResource = new WebResource(); + + webResource.method = "PUT"; + webResource.url = this.getUrl(path); + webResource.body = JSON.stringify(requestBody); + if (isUpdate) { + webResource.headers.set("If-Match", "*"); + } + webResource.headers.set("content-type", "application/atom+xml;type=entry;charset=utf-8"); + webResource.headers.set("content-length", Buffer.byteLength(webResource.body, "utf8")); + + const atomXmlOperationSpec: AtomXmlOperationSpec = { + serializer: serializer, + shouldParseResponse: false + }; + webResource.atomXmlOperationSpec = atomXmlOperationSpec; + + return this.sendRequest(webResource); + } + + /** + * Gets a resource. + * + * @param {string} path The resource path. + * @param {ResourceSerializer} serializer The XML serializer to use. + * @return {Promise} + */ + private async _getResource( + path: string, + serializer: ResourceSerializer + ): Promise { + let webResource: WebResource = new WebResource(); + + webResource.method = "GET"; + webResource.url = this.getUrl(path); + + const atomXmlOperationSpec: AtomXmlOperationSpec = { + serializer: serializer, + shouldParseResponse: true + }; + webResource.atomXmlOperationSpec = atomXmlOperationSpec; + + return this.sendRequest(webResource); + } + + /** + * Lists resources. + * + * @param {string} path The resource path. + * @param {object} requestBody The resource handler. + * @param {ResourceSerializer} serializer The XML serializer to use. + * @return {Promise} + */ + private async _listResources( + path: string, + requestBody: any, + serializer: ResourceSerializer + ): Promise { + let webResource: WebResource = new WebResource(); + + webResource.method = "GET"; + + const queryParams: any = {}; + if (requestBody) { + if (requestBody.skip) { + queryParams["$skip"] = requestBody.skip; + } + if (requestBody.top) { + queryParams["$top"] = requestBody.top; + } + } + + webResource.url = this.getUrl(path, queryParams); + + console.log(webResource.url); + + const atomXmlOperationSpec: AtomXmlOperationSpec = { + serializer: serializer, + shouldParseResponse: true + }; + webResource.atomXmlOperationSpec = atomXmlOperationSpec; + + return this.sendRequest(webResource); + } + + /** + * Lists resources. + * + * @param {string} path The resource path. + * @return {Promise} + */ + private async _deleteResource(path: any): Promise { + let webResource: WebResource = new WebResource(); + + webResource.method = "DELETE"; + webResource.url = this.getUrl(path); + + return this.sendRequest(webResource); + } + + private getUrl(path: string, queryParams?: any) { + const baseUri = `https://${this.endpoint}.servicebus.windows.net/${path}`; + + const requestUrl: URLBuilder = URLBuilder.parse(baseUri); + requestUrl.setQueryParameter(`api-version`, `2017-04`); + + if (queryParams) { + for (let key of Object.keys(queryParams)) { + requestUrl.setQueryParameter(key, queryParams[key]); + } + } + + return requestUrl.toString(); + } + private static parseConnectionString(connectionString: string): any { + const output: { [k: string]: string } = {}; + const parts = connectionString.trim().split(";"); + + for (let part of parts) { + part = part.trim(); + + if (part === "") { + // parts can be empty + continue; + } + + const splitIndex = part.indexOf("="); + if (splitIndex === -1) { + throw new Error( + "Connection string malformed: each part of the connection string must have an `=` assignment." + ); + } + + const key = part.substring(0, splitIndex).trim(); + if (key === "") { + throw new Error("Connection string malformed: missing key for assignment"); + } + + const value = part.substring(splitIndex + 1).trim(); + + output[key] = value; + } + + return output as any; + } + + private static getSubscriptionPath(topic: string, subscription: string) { + return topic + "/Subscriptions/" + subscription; + } + + private static getRulePath(topic: string, subscription: string, rule: string) { + return topic + "/Subscriptions/" + subscription + "/Rules/" + rule; + } +} diff --git a/sdk/servicebus/service-bus/src/util/constants.ts b/sdk/servicebus/service-bus/src/util/constants.ts index ccfa5fd210f3..3ed3f975daa0 100644 --- a/sdk/servicebus/service-bus/src/util/constants.ts +++ b/sdk/servicebus/service-bus/src/util/constants.ts @@ -9,3 +9,212 @@ export const packageJsonInfo = { export const messageDispositionTimeout = 20000; export const max32BitNumber = Math.pow(2, 31) - 1; + + +/** + * Defines constants for use with service bus. + * + * @const + * @type {string} + */ +export const ServiceBusAtomXmlConstants = { + /** + * The maximum size in megabytes. + * + * @const + * @type {string} + */ + MAX_SIZE_IN_MEGABYTES: "MaxSizeInMegabytes", + + /** + * The default message time to live. + * + * @const + * @type {string} + */ + DEFAULT_MESSAGE_TIME_TO_LIVE: "DefaultMessageTimeToLive", + + /** + * The lock duration. + * + * @const + * @type {string} + */ + LOCK_DURATION: "LockDuration", + + /** + * The indication if session is required or not. + * + * @const + * @type {string} + */ + REQUIRES_SESSION: "RequiresSession", + + /** + * The indication if duplicate detection is required or not. + * + * @const + * @type {string} + */ + REQUIRES_DUPLICATE_DETECTION: "RequiresDuplicateDetection", + + /** + * The indication if dead lettering on message expiration. + * + * @const + * @type {string} + */ + DEAD_LETTERING_ON_MESSAGE_EXPIRATION: "DeadLetteringOnMessageExpiration", + + /** + * The indication if dead lettering on filter evaluation exceptions. + * + * @const + * @type {string} + */ + DEAD_LETTERING_ON_FILTER_EVALUATION_EXCEPTIONS: "DeadLetteringOnFilterEvaluationExceptions", + + /** + * The history time window for duplicate detection. + * + * @const + * @type {string} + */ + DUPLICATE_DETECTION_HISTORY_TIME_WINDOW: "DuplicateDetectionHistoryTimeWindow", + + /** + * The maximum number of subscriptions per topic. + * + * @const + * @type {string} + */ + MAX_SUBSCRIPTIONS_PER_TOPIC: "MaxSubscriptionsPerTopic", + + /** + * The maximum amount of sql filters per topic. + * + * @const + * @type {string} + */ + MAX_SQL_FILTERS_PER_TOPIC: "MaxSqlFiltersPerTopic", + + /** + * The maximum amount of correlation filters per topic. + * + * @const + * @type {string} + */ + MAX_CORRELATION_FILTERS_PER_TOPIC: "MaxCorrelationFiltersPerTopic", + + /** + * The maximum delivery count. + * + * @const + * @type {string} + */ + MAX_DELIVERY_COUNT: "MaxDeliveryCount", + + /** + * Indicates if the queue has enabled batch operations. + * + * @const + * @type {string} + */ + ENABLE_BATCHED_OPERATIONS: "EnableBatchedOperations", + + /** + * Indicates whether the topic can be ordered + * + * @const + * @type {string} + */ + SUPPORT_ORDERING: "SupportOrdering", + + /** + * Indicates whether the topic/queue should be split across multiple partitions + * + * @const + * @type {string} + */ + ENABLE_PARTITIONING: "EnablePartitioning", + + /** + * Indicates the default rule description. + * + * @const + * @type {string} + */ + DEFAULT_RULE_DESCRIPTION: "DefaultRuleDescription", + + /** + * The queue's size in bytes. + * + * @const + * @type {string} + */ + SIZE_IN_BYTES: "SizeInBytes", + + /** + * The queue's message count. + * + * @const + * @type {string} + */ + MESSAGE_COUNT: "MessageCount", + + /** + * The default rule name. + * + * @const + * @type {string} + */ + DEFAULT_RULE_NAME: "$Default", + + /** + * The wrap access token. + * + * @const + * @type {string} + */ + WRAP_ACCESS_TOKEN: "wrap_access_token", + + /** + * The wrap access token expires utc. + * + * @const + * @type {string} + */ + WRAP_ACCESS_TOKEN_EXPIRES_UTC: "wrap_access_token_expires_utc", + + /** + * The wrap access token expires in. + * + * @const + * @type {string} + */ + WRAP_ACCESS_TOKEN_EXPIRES_IN: "wrap_access_token_expires_in", + + /** + * Max idle time before entity is deleted + * + * @const + * @type {string} + */ + AUTO_DELETE_ON_IDLE: "AutoDeleteOnIdle", + + /** + * Query string parameter to set Service Bus API version + * + * @const + * @type {string} + */ + API_VERSION_QUERY_KEY: "api-version", + + /** + * Current API version being sent to service bus + * + * @const + * @type {string} + */ + CURRENT_API_VERSION: "2016-07" +}; diff --git a/sdk/servicebus/service-bus/test/atomManagement.spec.ts b/sdk/servicebus/service-bus/test/atomManagement.spec.ts new file mode 100644 index 000000000000..7143397895a6 --- /dev/null +++ b/sdk/servicebus/service-bus/test/atomManagement.spec.ts @@ -0,0 +1,141 @@ +import { ServiceBusAtomManagementClient } from "../src"; +import { ServiceClientOptions } from "@azure/core-http"; + +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +chai.use(chaiAsPromised); +const should = chai.should(); + +let entityType: any; + +const clientOptions: ServiceClientOptions = { + requestPolicyFactories: [] +}; + +const serviceBusAtomManagementClient: ServiceBusAtomManagementClient = new ServiceBusAtomManagementClient( + "", + clientOptions +); + +const alwaysBeExistingQueue = "alwaysBeExistingQueue"; +const alwaysBeDeletedQueue = "alwaysBeDeletedQueue"; + +function prettyPrint(result: any) { + console.dir(`\n\nOperation result -> ${JSON.stringify(result, undefined, 2)}\n\n`); +} + +entityType = "Queue"; +describe(`Atom management - Basic CRUD on ${entityType} entities`, function(): void { + it(`Creates a non-existent ${entityType} entity successfully`, async () => { + await serviceBusAtomManagementClient.deleteQueue(alwaysBeExistingQueue); + + const response = await serviceBusAtomManagementClient.createQueue(alwaysBeExistingQueue, { + LockDuration: "PT1M", + MaxSizeInMegabytes: "1024", + RequiresDuplicateDetection: "false", + RequiresSession: "false", + DeadLetteringOnMessageExpiration: "false", + MaxDeliveryCount: "10", + EnableBatchedOperations: "true", + EnablePartitioning: "false" + }); + const result = response.parsedBody; + should.equal(result.error, undefined, "Error must be undefined"); + should.equal(result.result, undefined, "Result must be undefined for create requests"); + prettyPrint(result); + }); + + it(`Creating an existent ${entityType} entity throws an error`, async () => { + const response = await serviceBusAtomManagementClient.createQueue(alwaysBeExistingQueue, { + LockDuration: "PT1M", + MaxSizeInMegabytes: "1024", + RequiresDuplicateDetection: "false", + RequiresSession: "false", + DeadLetteringOnMessageExpiration: "false", + MaxDeliveryCount: "10", + EnableBatchedOperations: "true", + EnablePartitioning: "false" + }); + const result = response.parsedBody; + should.equal(result.error == undefined, false, "Error must not be undefined"); + prettyPrint(result); + }); + + it(`Lists available ${entityType} entities successfully`, async () => { + const response = await serviceBusAtomManagementClient.listQueues({ top: 10 }); + + const result = response.parsedBody; + should.equal(result.error, undefined, "Error must be undefined"); + should.equal(Array.isArray(result.result), true, "Result must be any array for list requests"); + prettyPrint(result); + }); + + it(`Updates an existent ${entityType} entity successfully`, async () => { + const response = await serviceBusAtomManagementClient.updateQueue(alwaysBeExistingQueue, { + LockDuration: "PT1M", + MaxSizeInMegabytes: "1024", + RequiresDuplicateDetection: "false", + RequiresSession: "false", + DeadLetteringOnMessageExpiration: "false", + MaxDeliveryCount: "10", + EnableBatchedOperations: "true", + EnablePartitioning: "false" + }); + + const result = response.parsedBody; + should.equal(result.error, undefined, "Error must be undefined"); + should.equal(result.result, undefined, "Result must be undefined for update() requests"); + prettyPrint(result); + }); + + it(`Gets an existent ${entityType} entity successfully`, async () => { + const response = await serviceBusAtomManagementClient.getQueue(alwaysBeExistingQueue); + + const result = response.parsedBody; + should.equal(result.error, undefined, "Error must be undefined"); + should.equal( + result.result == undefined, + false, + "Result must be NOT undefined for successful get request" + ); + prettyPrint(result); + }); + + it(`Deletes a non-existent ${entityType} entity returns an error`, async () => { + const response = await serviceBusAtomManagementClient.deleteQueue("notexisting"); + + const result = response.parsedBody; + should.equal(result.error == undefined, false, "Error must be NOT undefined"); + should.equal(result.result, undefined, "Result must be undefined for create() requests"); + prettyPrint(result); + }); + + it(`Deletes an existent ${entityType} entity successfully`, async () => { + await serviceBusAtomManagementClient.createQueue(alwaysBeDeletedQueue, { + LockDuration: "PT1M", + MaxSizeInMegabytes: "1024", + RequiresDuplicateDetection: "false", + RequiresSession: "false", + DeadLetteringOnMessageExpiration: "false", + MaxDeliveryCount: "10", + EnableBatchedOperations: "true", + EnablePartitioning: "false" + }); + const response = await serviceBusAtomManagementClient.deleteQueue(alwaysBeDeletedQueue); + + const result = response.parsedBody; + should.equal(result.error, undefined, "Error must be undefined"); + should.equal(result.result, undefined, "Result must be undefined for delete() requests"); + prettyPrint(result); + }); + + it(`Get on non-existent ${entityType} entity returns empty response`, async () => { + const response = await serviceBusAtomManagementClient.getQueue("notexisting"); + + const result = response.parsedBody; + should.equal(result.error, undefined, "Error must be undefined"); + should.equal(Array.isArray(result.result), true, "Result is array for empty get requests"); + should.equal(result.result.length, 0, "Array must be empty"); + prettyPrint(result); + }); +}); diff --git a/sdk/servicebus/service-bus/tsconfig.json b/sdk/servicebus/service-bus/tsconfig.json index 14b4f83f8a56..edfbda105832 100644 --- a/sdk/servicebus/service-bus/tsconfig.json +++ b/sdk/servicebus/service-bus/tsconfig.json @@ -3,7 +3,16 @@ /* Basic Options */ "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, "module": "es6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - "lib": [] /* lib dependencies are triple-slash directives in lib/index.ts */, + "lib": [ + "dom", + "dom.iterable", + "es5", + "es6", + "es7", + "esnext", + "esnext.asynciterable", + "es2015.iterable" + ] /* lib dependencies are triple-slash directives in lib/index.ts */, "declaration": true /* Generates corresponding '.d.ts' file. */, "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, "sourceMap": true /* Generates corresponding '.map' file. */, @@ -30,7 +39,8 @@ /* Other options */ "newLine": "LF" /* Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix).”*/, "allowJs": false /* Don't allow JavaScript files to be compiled.*/, - "resolveJsonModule": true + "resolveJsonModule": true, + "downlevelIteration": true }, "compileOnSave": true, "exclude": [ @@ -39,8 +49,5 @@ "./samples/**/*.ts", "./test/perf/azure-sb-package/*.ts" ], - "include": [ - "./src/**/*.ts", - "./test/**/*.ts" - ] + "include": ["./src/**/*.ts", "./test/**/*.ts"] }