diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1bfe0b87071b..589a894a84c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,7 +55,7 @@ /sdk/deviceupdate/ @dpokluda # PRLabel: %Digital Twins -/sdk/digitaltwins/ @johngallardo +/sdk/digitaltwins/ @johngallardo @YoDaMa @olivakar # PRLabel: %Event Grid /sdk/eventgrid/ @xirzec @ellismg @@ -119,6 +119,9 @@ # PRLabel: %Cognitive - Metrics Advisor /sdk/metricsadvisor/ @jeremymeng @KarishmaGhiya +# PRLabel: %Cognitive - Anomaly Detector +/sdk/anomalydetector/ @conhua @mengaims @juaduan @moreOver0 + # PRLabel: %Search /sdk/search/ @xirzec @sarangan12 diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index c91ec5747b87..515389f0d6d8 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -1,28 +1,25 @@ -**Checklists** -- [ ] Added impacted package name to the issue description - -**Packages impacted by this PR:** - -**Issues associated with this PR:** +### Packages impacted by this PR -**Describe the problem that is addressed by this PR:** +### Issues associated with this PR -**What are the possible designs available to address the problem** +### Describe the problem that is addressed by this PR -**If there are more than one possible design, why was the one in this PR chosen?** +### What are the possible designs available to address the problem? If there are more than one possible design, why was the one in this PR chosen? -**Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ +### Are there test cases added in this PR? _(If not, why?)_ -**Are there test cases added in this PR?**_(If not, why?)_ +### Provide a list of related PRs _(if any)_ -**Provide a list of related PRs**_(if any)_ +### Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ - -**Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ +### Checklists +- [ ] Added impacted package name to the issue description +- [ ] Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ +- [ ] Added a changelog (if necessary) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 22cc1340df64..515389f0d6d8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,29 +1,25 @@ -**Checklists** -- [ ] Added impacted package name to the issue description - -**Packages impacted by this PR:** - +### Packages impacted by this PR -**Issues associated with this PR:** +### Issues associated with this PR -**Describe the problem that is addressed by this PR:** +### Describe the problem that is addressed by this PR -**What are the possible designs available to address the problem** +### What are the possible designs available to address the problem? If there are more than one possible design, why was the one in this PR chosen? -**If there are more than one possible design, why was the one in this PR chosen?** +### Are there test cases added in this PR? _(If not, why?)_ -**Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ +### Provide a list of related PRs _(if any)_ -**Are there test cases added in this PR?**_(If not, why?)_ +### Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ -**Provide a list of related PRs**_(if any)_ - - -**Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ +### Checklists +- [ ] Added impacted package name to the issue description +- [ ] Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ +- [ ] Added a changelog (if necessary) diff --git a/common/tools/dev-tool/src/util/testProxyUtils.ts b/common/tools/dev-tool/src/util/testProxyUtils.ts index 99ffb8296c6f..191ee74494d6 100644 --- a/common/tools/dev-tool/src/util/testProxyUtils.ts +++ b/common/tools/dev-tool/src/util/testProxyUtils.ts @@ -5,6 +5,7 @@ import { spawn } from "child_process"; import path from "path"; import { IncomingMessage, request, RequestOptions } from "http"; import fs from "fs-extra"; +import os from "os"; import { createPrinter } from "./printer"; import { resolveRoot } from "./resolveProject"; @@ -25,6 +26,25 @@ export async function startProxyTool(): Promise { subprocess.stderr.pipe(out); log.info(`Check the output file "${outFileName}" for test-proxy logs.`); + + await new Promise((resolve, reject) => { + subprocess.on("exit", (code) => { + if (code === 0) { + resolve(); + } else { + fs.readFile(`./${outFileName}`, (_err, data) => { + const lines = data.toString().split(os.EOL); + reject( + new Error( + `Could not start test proxy. Below is the last 10 lines of output. See ${outFileName} for the full output.\n${lines + .slice(-10) + .join("\n")}` + ) + ); + }); + } + }); + }); } export async function stopProxyTool(): Promise { diff --git a/eng/common/docgeneration/Generate-DocIndex.ps1 b/eng/common/docgeneration/Generate-DocIndex.ps1 index 3c41d7e867bc..461b15ce01ff 100644 --- a/eng/common/docgeneration/Generate-DocIndex.ps1 +++ b/eng/common/docgeneration/Generate-DocIndex.ps1 @@ -119,7 +119,9 @@ function GenerateDocfxTocContent([Hashtable]$tocContent, [String]$lang, [String] $serviceName = $serviceMapping.Value[0] $displayName = $serviceMapping.Value[1] - $fileName = ($serviceName -replace '\s', '').ToLower().Trim() + # handle spaces in service name, EG "Confidential Ledger" + # handle / in service name, EG "Database for MySQL/PostgreSQL". Leaving a "/" present will generate a bad link location. + $fileName = ($serviceName -replace '\s', '').Replace("/","").ToLower().Trim() if ($visitedService.ContainsKey($serviceName)) { if ($displayName) { Add-Content -Path "$($YmlPath)/${fileName}.md" -Value "#### $artifact`n##### ($displayName)" diff --git a/eng/common/pipelines/templates/steps/verify-links.yml b/eng/common/pipelines/templates/steps/verify-links.yml index 5c4e7402bbd3..12e14d399128 100644 --- a/eng/common/pipelines/templates/steps/verify-links.yml +++ b/eng/common/pipelines/templates/steps/verify-links.yml @@ -6,7 +6,7 @@ parameters: Recursive: $false CheckLinkGuidance: $true Urls: '(Get-ChildItem -Path ./ -Recurse -Include *.md)' - BranchReplaceRegex: "^(${env:SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI}.*/(?:blob|tree)/)$(DefaultBranch)(/.*)$" + BranchReplaceRegex: "^(${env:SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI}/(?:blob|tree)/)$(DefaultBranch)(/.*)$" BranchReplacementName: "${env:SYSTEM_PULLREQUEST_SOURCECOMMITID}" Condition: succeeded() # If you want to run on failure for the link checker, set it to `Condition: succeededOrFailed()`. diff --git a/sdk/applicationinsights/arm-appinsights/CHANGELOG.md b/sdk/applicationinsights/arm-appinsights/CHANGELOG.md index 62470a621563..264f573b742c 100644 --- a/sdk/applicationinsights/arm-appinsights/CHANGELOG.md +++ b/sdk/applicationinsights/arm-appinsights/CHANGELOG.md @@ -1,8 +1,8 @@ # Release History -## 5.0.0-beta.1 (2022-01-21) +## 5.0.0-beta.2 (2022-01-24) -The package of @azure/arm-appinsights is using our next generation design principles since version 5.0.0-beta.1, which contains breaking changes. +The package of @azure/arm-appinsights is using our next generation design principles since version 5.0.0-beta.2, which contains breaking changes. To understand the detail of the change, please refer to [Changelog](https://aka.ms/js-track2-changelog). diff --git a/sdk/applicationinsights/arm-appinsights/_meta.json b/sdk/applicationinsights/arm-appinsights/_meta.json index 3021b5d9d2b5..c5db191fd6a1 100644 --- a/sdk/applicationinsights/arm-appinsights/_meta.json +++ b/sdk/applicationinsights/arm-appinsights/_meta.json @@ -1,5 +1,5 @@ { - "commit": "f9a6cb686bcc0f1b23761db19f2491c5c4df95cb", + "commit": "2a2aa10bc5e772934d22628faf45a53055ee96c6", "readme": "specification/applicationinsights/resource-manager/readme.md", "autorest_command": "autorest --version=3.7.3 --typescript --modelerfour.lenient-model-deduplication --head-as-boolean=true --license-header=MICROSOFT_MIT_NO_VERSION --generate-test --typescript-sdks-folder=D:\\mydev\\azure-sdk-for-js ../azure-rest-api-specs/specification/applicationinsights/resource-manager/readme.md --use=@autorest/typescript@6.0.0-alpha.16.20220114.1", "repository_url": "https://github.com/Azure/azure-rest-api-specs.git", diff --git a/sdk/applicationinsights/arm-appinsights/api-extractor.json b/sdk/applicationinsights/arm-appinsights/api-extractor.json index 8d94522c21e8..c1199353e8d1 100644 --- a/sdk/applicationinsights/arm-appinsights/api-extractor.json +++ b/sdk/applicationinsights/arm-appinsights/api-extractor.json @@ -1,18 +1,31 @@ { "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "mainEntryPointFilePath": "./dist-esm/src/index.d.ts", - "docModel": { "enabled": true }, - "apiReport": { "enabled": true, "reportFolder": "./review" }, + "docModel": { + "enabled": true + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, "dtsRollup": { "enabled": true, "untrimmedFilePath": "", "publicTrimmedFilePath": "./types/arm-appinsights.d.ts" }, "messages": { - "tsdocMessageReporting": { "default": { "logLevel": "none" } }, + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, "extractorMessageReporting": { - "ae-missing-release-tag": { "logLevel": "none" }, - "ae-unresolved-link": { "logLevel": "none" } + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } } } -} +} \ No newline at end of file diff --git a/sdk/applicationinsights/arm-appinsights/package.json b/sdk/applicationinsights/arm-appinsights/package.json index 50d5a2aa6c45..0f43f6e1e3e5 100644 --- a/sdk/applicationinsights/arm-appinsights/package.json +++ b/sdk/applicationinsights/arm-appinsights/package.json @@ -3,8 +3,10 @@ "sdk-type": "mgmt", "author": "Microsoft Corporation", "description": "A generated SDK for ApplicationInsightsManagementClient.", - "version": "5.0.0-beta.1", - "engines": { "node": ">=12.0.0" }, + "version": "5.0.0-beta.2", + "engines": { + "node": ">=12.0.0" + }, "dependencies": { "@azure/core-paging": "^1.2.0", "@azure/core-client": "^1.0.0", @@ -12,7 +14,13 @@ "@azure/core-rest-pipeline": "^1.1.0", "tslib": "^2.2.0" }, - "keywords": ["node", "azure", "typescript", "browser", "isomorphic"], + "keywords": [ + "node", + "azure", + "typescript", + "browser", + "isomorphic" + ], "license": "MIT", "main": "./dist/index.js", "module": "./dist-esm/src/index.js", @@ -39,7 +47,9 @@ "type": "git", "url": "https://github.com/Azure/azure-sdk-for-js.git" }, - "bugs": { "url": "https://github.com/Azure/azure-sdk-for-js/issues" }, + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, "files": [ "dist/**/*.js", "dist/**/*.js.map", @@ -95,4 +105,4 @@ ] }, "autoPublish": true -} +} \ No newline at end of file diff --git a/sdk/applicationinsights/arm-appinsights/review/arm-appinsights.api.md b/sdk/applicationinsights/arm-appinsights/review/arm-appinsights.api.md index 7b40e42381ee..0fbff25500c8 100644 --- a/sdk/applicationinsights/arm-appinsights/review/arm-appinsights.api.md +++ b/sdk/applicationinsights/arm-appinsights/review/arm-appinsights.api.md @@ -150,8 +150,10 @@ export type APIKeysListResponse = ApplicationInsightsComponentAPIKeyListResult; // @public export type ApplicationInsightsComponent = ComponentsResource & { kind: string; + etag?: string; readonly applicationId?: string; readonly appId?: string; + readonly namePropertiesName?: string; applicationType?: ApplicationType; flowType?: FlowType; requestSource?: RequestSource; @@ -166,10 +168,14 @@ export type ApplicationInsightsComponent = ComponentsResource & { retentionInDays?: number; disableIpMasking?: boolean; immediatePurgeDataOn30Days?: boolean; + workspaceResourceId?: string; + readonly laMigrationDate?: Date; readonly privateLinkScopedResources?: PrivateLinkScopedResource[]; publicNetworkAccessForIngestion?: PublicNetworkAccessType; publicNetworkAccessForQuery?: PublicNetworkAccessType; ingestionMode?: IngestionMode; + disableLocalAuth?: boolean; + forceCustomerStorageForProfiler?: boolean; }; // @public @@ -393,8 +399,6 @@ export class ApplicationInsightsManagementClient extends coreClient.ServiceClien // (undocumented) myWorkbooks: MyWorkbooks; // (undocumented) - operations: Operations; - // (undocumented) proactiveDetectionConfigurations: ProactiveDetectionConfigurations; // (undocumented) subscriptionId: string; @@ -656,6 +660,17 @@ export interface ErrorResponse { message?: string; } +// @public (undocumented) +export interface ErrorResponseComponents { + error?: ErrorResponseComponentsError; +} + +// @public +export interface ErrorResponseComponentsError { + readonly code?: string; + readonly message?: string; +} + // @public (undocumented) export interface ErrorResponseLinkedStorage { error?: ErrorResponseLinkedStorageError; @@ -1158,30 +1173,12 @@ export interface OperationListResult { // @public export interface OperationLive { display?: OperationInfo; + isDataAction?: boolean; name?: string; origin?: string; properties?: Record; } -// @public -export interface Operations { - list(options?: OperationsListOptionalParams): PagedAsyncIterableIterator; -} - -// @public -export interface OperationsListNextOptionalParams extends coreClient.OperationOptions { -} - -// @public -export type OperationsListNextResponse = OperationsListResult; - -// @public -export interface OperationsListOptionalParams extends coreClient.OperationOptions { -} - -// @public -export type OperationsListResponse = OperationsListResult; - // @public export interface OperationsListResult { nextLink?: string; diff --git a/sdk/applicationinsights/arm-appinsights/src/applicationInsightsManagementClient.ts b/sdk/applicationinsights/arm-appinsights/src/applicationInsightsManagementClient.ts index 94ec76c781ce..2039867a000d 100644 --- a/sdk/applicationinsights/arm-appinsights/src/applicationInsightsManagementClient.ts +++ b/sdk/applicationinsights/arm-appinsights/src/applicationInsightsManagementClient.ts @@ -27,7 +27,6 @@ import { WorkbooksImpl, ComponentsImpl, ComponentLinkedStorageAccountsOperationsImpl, - OperationsImpl, LiveTokenImpl } from "./operations"; import { @@ -49,7 +48,6 @@ import { Workbooks, Components, ComponentLinkedStorageAccountsOperations, - Operations, LiveToken } from "./operationsInterfaces"; import { ApplicationInsightsManagementClientOptionalParams } from "./models"; @@ -85,7 +83,7 @@ export class ApplicationInsightsManagementClient extends coreClient.ServiceClien credential: credentials }; - const packageDetails = `azsdk-js-arm-appinsights/5.0.0-beta.1`; + const packageDetails = `azsdk-js-arm-appinsights/5.0.0-beta.2`; const userAgentPrefix = options.userAgentOptions && options.userAgentOptions.userAgentPrefix ? `${options.userAgentOptions.userAgentPrefix} ${packageDetails}` @@ -134,7 +132,6 @@ export class ApplicationInsightsManagementClient extends coreClient.ServiceClien this.componentLinkedStorageAccountsOperations = new ComponentLinkedStorageAccountsOperationsImpl( this ); - this.operations = new OperationsImpl(this); this.liveToken = new LiveTokenImpl(this); } @@ -156,6 +153,5 @@ export class ApplicationInsightsManagementClient extends coreClient.ServiceClien workbooks: Workbooks; components: Components; componentLinkedStorageAccountsOperations: ComponentLinkedStorageAccountsOperations; - operations: Operations; liveToken: LiveToken; } diff --git a/sdk/applicationinsights/arm-appinsights/src/models/index.ts b/sdk/applicationinsights/arm-appinsights/src/models/index.ts index 079a9c7f62ac..2ff1b1ccae2b 100644 --- a/sdk/applicationinsights/arm-appinsights/src/models/index.ts +++ b/sdk/applicationinsights/arm-appinsights/src/models/index.ts @@ -980,6 +980,25 @@ export interface ComponentsResource { tags?: { [propertyName: string]: string }; } +export interface ErrorResponseComponents { + /** Error response indicates Insights service is not able to process the incoming request. The reason is provided in the error message. */ + error?: ErrorResponseComponentsError; +} + +/** Error response indicates Insights service is not able to process the incoming request. The reason is provided in the error message. */ +export interface ErrorResponseComponentsError { + /** + * Error code. + * NOTE: This property will not be serialized. It can only be populated by the server. + */ + readonly code?: string; + /** + * Error message indicating why the operation failed. + * NOTE: This property will not be serialized. It can only be populated by the server. + */ + readonly message?: string; +} + /** Describes the body of a purge request for an App Insights component */ export interface ComponentPurgeBody { /** Table from which to purge data. */ @@ -1037,38 +1056,6 @@ export interface ComponentLinkedStorageAccountsPatch { linkedStorageAccount?: string; } -/** Result of the List Operations operation */ -export interface OperationsListResult { - /** A collection of operations */ - value?: OperationLive[]; - /** URL to get the next set of operation list results if there are any. */ - nextLink?: string; -} - -/** Represents an operation returned by the GetOperations request */ -export interface OperationLive { - /** Name of the operation */ - name?: string; - /** Display name of the operation */ - display?: OperationInfo; - /** Origin of the operation */ - origin?: string; - /** Properties of the operation */ - properties?: Record; -} - -/** Information about an operation */ -export interface OperationInfo { - /** Name of the provider */ - provider?: string; - /** Name of the resource type */ - resource?: string; - /** Name of the operation */ - operation?: string; - /** Description of the operation */ - description?: string; -} - /** The response to a live token query. */ export interface LiveTokenResponse { /** @@ -1130,6 +1117,40 @@ export interface WorkbookInnerErrorTrace { readonly trace?: string[]; } +/** Result of the List Operations operation */ +export interface OperationsListResult { + /** A collection of operations */ + value?: OperationLive[]; + /** URL to get the next set of operation list results if there are any. */ + nextLink?: string; +} + +/** Represents an operation returned by the GetOperations request */ +export interface OperationLive { + /** Name of the operation */ + name?: string; + /** Indicates whether the operation is a data action */ + isDataAction?: boolean; + /** Display name of the operation */ + display?: OperationInfo; + /** Origin of the operation */ + origin?: string; + /** Properties of the operation */ + properties?: Record; +} + +/** Information about an operation */ +export interface OperationInfo { + /** Name of the provider */ + provider?: string; + /** Name of the resource type */ + resource?: string; + /** Name of the operation */ + operation?: string; + /** Description of the operation */ + description?: string; +} + /** An Application Insights web test definition. */ export type WebTest = WebtestsResource & { /** The kind of web test that this web test watches. Choices are ping and multistep. */ @@ -1228,6 +1249,8 @@ export type ProxyResource = Resource & {}; export type ApplicationInsightsComponent = ComponentsResource & { /** The kind of application that this component refers to, used to customize UI. This value is a freeform string, values should typically be one of the following: web, ios, other, store, java, phone. */ kind: string; + /** Resource etag */ + etag?: string; /** * The unique ID of your application. This field mirrors the 'Name' field and cannot be changed. * NOTE: This property will not be serialized. It can only be populated by the server. @@ -1238,6 +1261,11 @@ export type ApplicationInsightsComponent = ComponentsResource & { * NOTE: This property will not be serialized. It can only be populated by the server. */ readonly appId?: string; + /** + * Application name. + * NOTE: This property will not be serialized. It can only be populated by the server. + */ + readonly namePropertiesName?: string; /** Type of application being monitored. */ applicationType?: ApplicationType; /** Used by the Application Insights system to determine what kind of flow this component was created by. This is to be set to 'Bluefield' when creating/updating a component via the REST API. */ @@ -1284,6 +1312,13 @@ export type ApplicationInsightsComponent = ComponentsResource & { disableIpMasking?: boolean; /** Purge data immediately after 30 days. */ immediatePurgeDataOn30Days?: boolean; + /** Resource Id of the log analytics workspace which the data will be ingested to. This property is required to create an application with this API version. Applications from older versions will not have this property. */ + workspaceResourceId?: string; + /** + * The date which the component got migrated to LA, in ISO 8601 format. + * NOTE: This property will not be serialized. It can only be populated by the server. + */ + readonly laMigrationDate?: Date; /** * List of linked private link scope resources. * NOTE: This property will not be serialized. It can only be populated by the server. @@ -1295,6 +1330,10 @@ export type ApplicationInsightsComponent = ComponentsResource & { publicNetworkAccessForQuery?: PublicNetworkAccessType; /** Indicates the flow of the ingestion. */ ingestionMode?: IngestionMode; + /** Disable Non-AAD based Auth. */ + disableLocalAuth?: boolean; + /** Force users to create their own storage account for profiler and debugger. */ + forceCustomerStorageForProfiler?: boolean; }; /** An azure resource object */ @@ -2335,20 +2374,6 @@ export type ComponentLinkedStorageAccountsUpdateResponse = ComponentLinkedStorag export interface ComponentLinkedStorageAccountsDeleteOptionalParams extends coreClient.OperationOptions {} -/** Optional parameters. */ -export interface OperationsListOptionalParams - extends coreClient.OperationOptions {} - -/** Contains response data for the list operation. */ -export type OperationsListResponse = OperationsListResult; - -/** Optional parameters. */ -export interface OperationsListNextOptionalParams - extends coreClient.OperationOptions {} - -/** Contains response data for the listNext operation. */ -export type OperationsListNextResponse = OperationsListResult; - /** Optional parameters. */ export interface LiveTokenGetOptionalParams extends coreClient.OperationOptions {} diff --git a/sdk/applicationinsights/arm-appinsights/src/models/mappers.ts b/sdk/applicationinsights/arm-appinsights/src/models/mappers.ts index bae6de63330d..1b0a3199878a 100644 --- a/sdk/applicationinsights/arm-appinsights/src/models/mappers.ts +++ b/sdk/applicationinsights/arm-appinsights/src/models/mappers.ts @@ -2196,6 +2196,45 @@ export const ComponentsResource: coreClient.CompositeMapper = { } }; +export const ErrorResponseComponents: coreClient.CompositeMapper = { + type: { + name: "Composite", + className: "ErrorResponseComponents", + modelProperties: { + error: { + serializedName: "error", + type: { + name: "Composite", + className: "ErrorResponseComponentsError" + } + } + } + } +}; + +export const ErrorResponseComponentsError: coreClient.CompositeMapper = { + type: { + name: "Composite", + className: "ErrorResponseComponentsError", + modelProperties: { + code: { + serializedName: "code", + readOnly: true, + type: { + name: "String" + } + }, + message: { + serializedName: "message", + readOnly: true, + type: { + name: "String" + } + } + } + } +}; + export const ComponentPurgeBody: coreClient.CompositeMapper = { type: { name: "Composite", @@ -2344,101 +2383,6 @@ export const ComponentLinkedStorageAccountsPatch: coreClient.CompositeMapper = { } }; -export const OperationsListResult: coreClient.CompositeMapper = { - type: { - name: "Composite", - className: "OperationsListResult", - modelProperties: { - value: { - serializedName: "value", - type: { - name: "Sequence", - element: { - type: { - name: "Composite", - className: "OperationLive" - } - } - } - }, - nextLink: { - serializedName: "nextLink", - type: { - name: "String" - } - } - } - } -}; - -export const OperationLive: coreClient.CompositeMapper = { - type: { - name: "Composite", - className: "OperationLive", - modelProperties: { - name: { - serializedName: "name", - type: { - name: "String" - } - }, - display: { - serializedName: "display", - type: { - name: "Composite", - className: "OperationInfo" - } - }, - origin: { - serializedName: "origin", - type: { - name: "String" - } - }, - properties: { - serializedName: "properties", - type: { - name: "Dictionary", - value: { type: { name: "any" } } - } - } - } - } -}; - -export const OperationInfo: coreClient.CompositeMapper = { - type: { - name: "Composite", - className: "OperationInfo", - modelProperties: { - provider: { - serializedName: "provider", - type: { - name: "String" - } - }, - resource: { - serializedName: "resource", - type: { - name: "String" - } - }, - operation: { - serializedName: "operation", - type: { - name: "String" - } - }, - description: { - serializedName: "description", - type: { - name: "String" - } - } - } - } -}; - export const LiveTokenResponse: coreClient.CompositeMapper = { type: { name: "Composite", @@ -2594,6 +2538,107 @@ export const WorkbookInnerErrorTrace: coreClient.CompositeMapper = { } }; +export const OperationsListResult: coreClient.CompositeMapper = { + type: { + name: "Composite", + className: "OperationsListResult", + modelProperties: { + value: { + serializedName: "value", + type: { + name: "Sequence", + element: { + type: { + name: "Composite", + className: "OperationLive" + } + } + } + }, + nextLink: { + serializedName: "nextLink", + type: { + name: "String" + } + } + } + } +}; + +export const OperationLive: coreClient.CompositeMapper = { + type: { + name: "Composite", + className: "OperationLive", + modelProperties: { + name: { + serializedName: "name", + type: { + name: "String" + } + }, + isDataAction: { + serializedName: "isDataAction", + type: { + name: "Boolean" + } + }, + display: { + serializedName: "display", + type: { + name: "Composite", + className: "OperationInfo" + } + }, + origin: { + serializedName: "origin", + type: { + name: "String" + } + }, + properties: { + serializedName: "properties", + type: { + name: "Dictionary", + value: { type: { name: "any" } } + } + } + } + } +}; + +export const OperationInfo: coreClient.CompositeMapper = { + type: { + name: "Composite", + className: "OperationInfo", + modelProperties: { + provider: { + serializedName: "provider", + type: { + name: "String" + } + }, + resource: { + serializedName: "resource", + type: { + name: "String" + } + }, + operation: { + serializedName: "operation", + type: { + name: "String" + } + }, + description: { + serializedName: "description", + type: { + name: "String" + } + } + } + } +}; + export const WebTest: coreClient.CompositeMapper = { type: { name: "Composite", @@ -2889,6 +2934,12 @@ export const ApplicationInsightsComponent: coreClient.CompositeMapper = { name: "String" } }, + etag: { + serializedName: "etag", + type: { + name: "String" + } + }, applicationId: { serializedName: "properties.ApplicationId", readOnly: true, @@ -2903,6 +2954,13 @@ export const ApplicationInsightsComponent: coreClient.CompositeMapper = { name: "String" } }, + namePropertiesName: { + serializedName: "properties.Name", + readOnly: true, + type: { + name: "String" + } + }, applicationType: { defaultValue: "web", serializedName: "properties.Application_Type", @@ -2979,7 +3037,6 @@ export const ApplicationInsightsComponent: coreClient.CompositeMapper = { } }, retentionInDays: { - defaultValue: 90, serializedName: "properties.RetentionInDays", type: { name: "Number" @@ -2997,6 +3054,19 @@ export const ApplicationInsightsComponent: coreClient.CompositeMapper = { name: "Boolean" } }, + workspaceResourceId: { + serializedName: "properties.WorkspaceResourceId", + type: { + name: "String" + } + }, + laMigrationDate: { + serializedName: "properties.LaMigrationDate", + readOnly: true, + type: { + name: "DateTime" + } + }, privateLinkScopedResources: { serializedName: "properties.PrivateLinkScopedResources", readOnly: true, @@ -3025,11 +3095,23 @@ export const ApplicationInsightsComponent: coreClient.CompositeMapper = { } }, ingestionMode: { - defaultValue: "ApplicationInsights", + defaultValue: "LogAnalytics", serializedName: "properties.IngestionMode", type: { name: "String" } + }, + disableLocalAuth: { + serializedName: "properties.DisableLocalAuth", + type: { + name: "Boolean" + } + }, + forceCustomerStorageForProfiler: { + serializedName: "properties.ForceCustomerStorageForProfiler", + type: { + name: "Boolean" + } } } } diff --git a/sdk/applicationinsights/arm-appinsights/src/models/parameters.ts b/sdk/applicationinsights/arm-appinsights/src/models/parameters.ts index ee1eede7e29b..6efff1734966 100644 --- a/sdk/applicationinsights/arm-appinsights/src/models/parameters.ts +++ b/sdk/applicationinsights/arm-appinsights/src/models/parameters.ts @@ -508,7 +508,7 @@ export const revisionId: OperationURLParameter = { export const apiVersion4: OperationQueryParameter = { parameterPath: "apiVersion", mapper: { - defaultValue: "2018-05-01-preview", + defaultValue: "2020-02-02", isConstant: true, serializedName: "api-version", type: { @@ -576,18 +576,6 @@ export const linkedStorageAccountsProperties1: OperationParameter = { mapper: ComponentLinkedStorageAccountsPatchMapper }; -export const apiVersion6: OperationQueryParameter = { - parameterPath: "apiVersion", - mapper: { - defaultValue: "2020-06-02-preview", - isConstant: true, - serializedName: "api-version", - type: { - name: "String" - } - } -}; - export const resourceUri: OperationURLParameter = { parameterPath: "resourceUri", mapper: { @@ -599,3 +587,15 @@ export const resourceUri: OperationURLParameter = { }, skipEncoding: true }; + +export const apiVersion6: OperationQueryParameter = { + parameterPath: "apiVersion", + mapper: { + defaultValue: "2021-10-14", + isConstant: true, + serializedName: "api-version", + type: { + name: "String" + } + } +}; diff --git a/sdk/applicationinsights/arm-appinsights/src/operations/components.ts b/sdk/applicationinsights/arm-appinsights/src/operations/components.ts index c64a1cb7411c..d4f18070132c 100644 --- a/sdk/applicationinsights/arm-appinsights/src/operations/components.ts +++ b/sdk/applicationinsights/arm-appinsights/src/operations/components.ts @@ -331,6 +331,9 @@ const listOperationSpec: coreClient.OperationSpec = { responses: { 200: { bodyMapper: Mappers.ApplicationInsightsComponentListResult + }, + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, queryParameters: [Parameters.apiVersion4], @@ -345,6 +348,9 @@ const listByResourceGroupOperationSpec: coreClient.OperationSpec = { responses: { 200: { bodyMapper: Mappers.ApplicationInsightsComponentListResult + }, + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, queryParameters: [Parameters.apiVersion4], @@ -360,7 +366,13 @@ const deleteOperationSpec: coreClient.OperationSpec = { path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Insights/components/{resourceName}", httpMethod: "DELETE", - responses: { 200: {}, 204: {} }, + responses: { + 200: {}, + 204: {}, + default: { + bodyMapper: Mappers.ErrorResponseComponents + } + }, queryParameters: [Parameters.apiVersion4], urlParameters: [ Parameters.$host, @@ -368,6 +380,7 @@ const deleteOperationSpec: coreClient.OperationSpec = { Parameters.subscriptionId, Parameters.resourceName ], + headerParameters: [Parameters.accept], serializer }; const getOperationSpec: coreClient.OperationSpec = { @@ -377,6 +390,9 @@ const getOperationSpec: coreClient.OperationSpec = { responses: { 200: { bodyMapper: Mappers.ApplicationInsightsComponent + }, + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, queryParameters: [Parameters.apiVersion4], @@ -397,8 +413,8 @@ const createOrUpdateOperationSpec: coreClient.OperationSpec = { 200: { bodyMapper: Mappers.ApplicationInsightsComponent }, - 201: { - bodyMapper: Mappers.ApplicationInsightsComponent + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, requestBody: Parameters.insightProperties, @@ -421,8 +437,8 @@ const updateTagsOperationSpec: coreClient.OperationSpec = { 200: { bodyMapper: Mappers.ApplicationInsightsComponent }, - 201: { - bodyMapper: Mappers.ApplicationInsightsComponent + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, requestBody: Parameters.componentTags, @@ -444,6 +460,9 @@ const purgeOperationSpec: coreClient.OperationSpec = { responses: { 202: { bodyMapper: Mappers.ComponentPurgeResponse + }, + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, requestBody: Parameters.body, @@ -465,6 +484,9 @@ const getPurgeStatusOperationSpec: coreClient.OperationSpec = { responses: { 200: { bodyMapper: Mappers.ComponentPurgeStatusResponse + }, + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, queryParameters: [Parameters.apiVersion4], @@ -484,6 +506,9 @@ const listNextOperationSpec: coreClient.OperationSpec = { responses: { 200: { bodyMapper: Mappers.ApplicationInsightsComponentListResult + }, + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, queryParameters: [Parameters.apiVersion4], @@ -501,6 +526,9 @@ const listByResourceGroupNextOperationSpec: coreClient.OperationSpec = { responses: { 200: { bodyMapper: Mappers.ApplicationInsightsComponentListResult + }, + default: { + bodyMapper: Mappers.ErrorResponseComponents } }, queryParameters: [Parameters.apiVersion4], diff --git a/sdk/applicationinsights/arm-appinsights/src/operations/index.ts b/sdk/applicationinsights/arm-appinsights/src/operations/index.ts index 22f0c430dc3b..d273af781cd5 100644 --- a/sdk/applicationinsights/arm-appinsights/src/operations/index.ts +++ b/sdk/applicationinsights/arm-appinsights/src/operations/index.ts @@ -24,5 +24,4 @@ export * from "./myWorkbooks"; export * from "./workbooks"; export * from "./components"; export * from "./componentLinkedStorageAccountsOperations"; -export * from "./operations"; export * from "./liveToken"; diff --git a/sdk/applicationinsights/arm-appinsights/src/operations/liveToken.ts b/sdk/applicationinsights/arm-appinsights/src/operations/liveToken.ts index 7b2d791f2b37..b5e5efb50c24 100644 --- a/sdk/applicationinsights/arm-appinsights/src/operations/liveToken.ts +++ b/sdk/applicationinsights/arm-appinsights/src/operations/liveToken.ts @@ -44,7 +44,7 @@ export class LiveTokenImpl implements LiveToken { const serializer = coreClient.createSerializer(Mappers, /* isXml */ false); const getOperationSpec: coreClient.OperationSpec = { - path: "/{resourceUri}/providers/microsoft.insights/generatelivetoken", + path: "/{resourceUri}/providers/Microsoft.Insights/generatelivetoken", httpMethod: "POST", responses: { 200: { diff --git a/sdk/applicationinsights/arm-appinsights/src/operations/operations.ts b/sdk/applicationinsights/arm-appinsights/src/operations/operations.ts deleted file mode 100644 index 085c42a0403f..000000000000 --- a/sdk/applicationinsights/arm-appinsights/src/operations/operations.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is regenerated. - */ - -import { PagedAsyncIterableIterator } from "@azure/core-paging"; -import { Operations } from "../operationsInterfaces"; -import * as coreClient from "@azure/core-client"; -import * as Mappers from "../models/mappers"; -import * as Parameters from "../models/parameters"; -import { ApplicationInsightsManagementClient } from "../applicationInsightsManagementClient"; -import { - OperationLive, - OperationsListNextOptionalParams, - OperationsListOptionalParams, - OperationsListResponse, - OperationsListNextResponse -} from "../models"; - -/// -/** Class containing Operations operations. */ -export class OperationsImpl implements Operations { - private readonly client: ApplicationInsightsManagementClient; - - /** - * Initialize a new instance of the class Operations class. - * @param client Reference to the service client - */ - constructor(client: ApplicationInsightsManagementClient) { - this.client = client; - } - - /** - * List the available operations supported by the resource provider. - * @param options The options parameters. - */ - public list( - options?: OperationsListOptionalParams - ): PagedAsyncIterableIterator { - const iter = this.listPagingAll(options); - return { - next() { - return iter.next(); - }, - [Symbol.asyncIterator]() { - return this; - }, - byPage: () => { - return this.listPagingPage(options); - } - }; - } - - private async *listPagingPage( - options?: OperationsListOptionalParams - ): AsyncIterableIterator { - let result = await this._list(options); - yield result.value || []; - let continuationToken = result.nextLink; - while (continuationToken) { - result = await this._listNext(continuationToken, options); - continuationToken = result.nextLink; - yield result.value || []; - } - } - - private async *listPagingAll( - options?: OperationsListOptionalParams - ): AsyncIterableIterator { - for await (const page of this.listPagingPage(options)) { - yield* page; - } - } - - /** - * List the available operations supported by the resource provider. - * @param options The options parameters. - */ - private _list( - options?: OperationsListOptionalParams - ): Promise { - return this.client.sendOperationRequest({ options }, listOperationSpec); - } - - /** - * ListNext - * @param nextLink The nextLink from the previous successful call to the List method. - * @param options The options parameters. - */ - private _listNext( - nextLink: string, - options?: OperationsListNextOptionalParams - ): Promise { - return this.client.sendOperationRequest( - { nextLink, options }, - listNextOperationSpec - ); - } -} -// Operation Specifications -const serializer = coreClient.createSerializer(Mappers, /* isXml */ false); - -const listOperationSpec: coreClient.OperationSpec = { - path: "/providers/microsoft.insights/operations", - httpMethod: "GET", - responses: { - 200: { - bodyMapper: Mappers.OperationsListResult - }, - default: {} - }, - queryParameters: [Parameters.apiVersion6], - urlParameters: [Parameters.$host], - headerParameters: [Parameters.accept], - serializer -}; -const listNextOperationSpec: coreClient.OperationSpec = { - path: "{nextLink}", - httpMethod: "GET", - responses: { - 200: { - bodyMapper: Mappers.OperationsListResult - }, - default: {} - }, - queryParameters: [Parameters.apiVersion6], - urlParameters: [Parameters.$host, Parameters.nextLink], - headerParameters: [Parameters.accept], - serializer -}; diff --git a/sdk/applicationinsights/arm-appinsights/src/operationsInterfaces/index.ts b/sdk/applicationinsights/arm-appinsights/src/operationsInterfaces/index.ts index 22f0c430dc3b..d273af781cd5 100644 --- a/sdk/applicationinsights/arm-appinsights/src/operationsInterfaces/index.ts +++ b/sdk/applicationinsights/arm-appinsights/src/operationsInterfaces/index.ts @@ -24,5 +24,4 @@ export * from "./myWorkbooks"; export * from "./workbooks"; export * from "./components"; export * from "./componentLinkedStorageAccountsOperations"; -export * from "./operations"; export * from "./liveToken"; diff --git a/sdk/applicationinsights/arm-appinsights/src/operationsInterfaces/operations.ts b/sdk/applicationinsights/arm-appinsights/src/operationsInterfaces/operations.ts deleted file mode 100644 index 4578cf770899..000000000000 --- a/sdk/applicationinsights/arm-appinsights/src/operationsInterfaces/operations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is regenerated. - */ - -import { PagedAsyncIterableIterator } from "@azure/core-paging"; -import { OperationLive, OperationsListOptionalParams } from "../models"; - -/// -/** Interface representing a Operations. */ -export interface Operations { - /** - * List the available operations supported by the resource provider. - * @param options The options parameters. - */ - list( - options?: OperationsListOptionalParams - ): PagedAsyncIterableIterator; -} diff --git a/sdk/applicationinsights/arm-appinsights/tsconfig.json b/sdk/applicationinsights/arm-appinsights/tsconfig.json index 6e3251194117..3e6ae96443f3 100644 --- a/sdk/applicationinsights/arm-appinsights/tsconfig.json +++ b/sdk/applicationinsights/arm-appinsights/tsconfig.json @@ -9,11 +9,19 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, - "lib": ["es6", "dom"], + "lib": [ + "es6", + "dom" + ], "declaration": true, "outDir": "./dist-esm", "importHelpers": true }, - "include": ["./src/**/*.ts", "./test/**/*.ts"], - "exclude": ["node_modules"] -} + "include": [ + "./src/**/*.ts", + "./test/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/sdk/compute/arm-compute/CHANGELOG.md b/sdk/compute/arm-compute/CHANGELOG.md index 37aad16a6bf3..3e45fa8a32ce 100644 --- a/sdk/compute/arm-compute/CHANGELOG.md +++ b/sdk/compute/arm-compute/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History - + +## 17.1.1 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 17.1.0 (2022-01-06) **Features** diff --git a/sdk/compute/arm-compute/package.json b/sdk/compute/arm-compute/package.json index 9013eea2ff8e..57fba04bbcec 100644 --- a/sdk/compute/arm-compute/package.json +++ b/sdk/compute/arm-compute/package.json @@ -3,8 +3,10 @@ "sdk-type": "mgmt", "author": "Microsoft Corporation", "description": "A generated SDK for ComputeManagementClient.", - "version": "17.1.0", - "engines": { "node": ">=12.0.0" }, + "version": "17.1.1", + "engines": { + "node": ">=12.0.0" + }, "dependencies": { "@azure/core-lro": "^2.2.0", "@azure/abort-controller": "^1.0.0", @@ -14,7 +16,13 @@ "@azure/core-rest-pipeline": "^1.1.0", "tslib": "^2.2.0" }, - "keywords": ["node", "azure", "typescript", "browser", "isomorphic"], + "keywords": [ + "node", + "azure", + "typescript", + "browser", + "isomorphic" + ], "license": "MIT", "main": "./dist/index.js", "module": "./dist-esm/src/index.js", @@ -42,7 +50,9 @@ "type": "git", "url": "https://github.com/Azure/azure-sdk-for-js.git" }, - "bugs": { "url": "https://github.com/Azure/azure-sdk-for-js/issues" }, + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, "files": [ "dist/**/*.js", "dist/**/*.js.map", diff --git a/sdk/core/core-amqp/package.json b/sdk/core/core-amqp/package.json index d1bbfe6c34bd..0578cfddaa92 100644 --- a/sdk/core/core-amqp/package.json +++ b/sdk/core/core-amqp/package.json @@ -43,7 +43,7 @@ "clean": "rimraf dist dist-* temp types coverage coverage-browser .nyc_output *.tgz *.log test*.xml", "execute:samples": "echo skipped", "extract-api": "tsc -p . && api-extractor run --local", - "format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", "integration-test:browser": "echo skipped", "integration-test:node": "echo skipped", "integration-test": "npm run integration-test:node && npm run integration-test:browser", diff --git a/sdk/core/core-amqp/src/util/utils.ts b/sdk/core/core-amqp/src/util/utils.ts index ac73f74e3eb1..68333b02b7b8 100644 --- a/sdk/core/core-amqp/src/util/utils.ts +++ b/sdk/core/core-amqp/src/util/utils.ts @@ -58,7 +58,7 @@ export interface WebSocketOptions { * A constant that indicates whether the environment is node.js or browser based. */ export const isNode = - !!process && !!process.version && !!process.versions && !!process.versions.node; + typeof process !== "undefined" && Boolean(process.version) && Boolean(process.versions?.node); /** * Defines an object with possible properties defined in T. diff --git a/sdk/deploymentmanager/arm-deploymentmanager/CHANGELOG.md b/sdk/deploymentmanager/arm-deploymentmanager/CHANGELOG.md index 4a2c4d9abfea..dd1d6b378ad5 100644 --- a/sdk/deploymentmanager/arm-deploymentmanager/CHANGELOG.md +++ b/sdk/deploymentmanager/arm-deploymentmanager/CHANGELOG.md @@ -1,14 +1,10 @@ # Release History -## 4.0.0-beta.2 (Unreleased) +## 4.0.0-beta.2 (2022-01-26) -### Features Added +**Feature** -### Breaking Changes - -### Bugs Fixed - -### Other Changes + - Update link ## 4.0.0-beta.1 (2022-01-17) diff --git a/sdk/deploymentmanager/arm-deploymentmanager/README.md b/sdk/deploymentmanager/arm-deploymentmanager/README.md index f06fdeceaa50..6cbf79c29f59 100644 --- a/sdk/deploymentmanager/arm-deploymentmanager/README.md +++ b/sdk/deploymentmanager/arm-deploymentmanager/README.md @@ -2,7 +2,7 @@ This package contains an isomorphic SDK (runs both in Node.js and in browsers) for Azure Service client. -REST APIs for orchestrating deployments using the Azure Deployment Manager (ADM). See https://docs.microsoft.com/en-us/azure/azure-resource-manager/deployment-manager-overview for more information. +REST APIs for orchestrating deployments using the Azure Deployment Manager (ADM). See https://docs.microsoft.com/azure/azure-resource-manager/deployment-manager-overview for more information. [Source code](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/deploymentmanager/arm-deploymentmanager) | [Package (NPM)](https://www.npmjs.com/package/@azure/arm-deploymentmanager) | diff --git a/sdk/deploymentmanager/arm-deploymentmanager/src/azureDeploymentManager.ts b/sdk/deploymentmanager/arm-deploymentmanager/src/azureDeploymentManager.ts index d32ad298bc1e..ffe6ef7bd9b1 100644 --- a/sdk/deploymentmanager/arm-deploymentmanager/src/azureDeploymentManager.ts +++ b/sdk/deploymentmanager/arm-deploymentmanager/src/azureDeploymentManager.ts @@ -61,7 +61,7 @@ export class AzureDeploymentManager extends coreClient.ServiceClient { credential: credentials }; - const packageDetails = `azsdk-js-arm-deploymentmanager/4.0.0-beta.1`; + const packageDetails = `azsdk-js-arm-deploymentmanager/4.0.0-beta.2`; const userAgentPrefix = options.userAgentOptions && options.userAgentOptions.userAgentPrefix ? `${options.userAgentOptions.userAgentPrefix} ${packageDetails}` diff --git a/sdk/eventhub/event-hubs/review/event-hubs.api.md b/sdk/eventhub/event-hubs/review/event-hubs.api.md index 6d028766d38c..03c3baec2ee0 100644 --- a/sdk/eventhub/event-hubs/review/event-hubs.api.md +++ b/sdk/eventhub/event-hubs/review/event-hubs.api.md @@ -61,6 +61,9 @@ export interface CreateBatchOptions extends OperationOptions { partitionKey?: string; } +// @public +export function createEventDataAdapter(params?: EventDataAdapterParameters): MessageAdapter; + // @public export const earliestEventPosition: EventPosition; @@ -79,6 +82,15 @@ export interface EventData { }; } +// @public +export interface EventDataAdapterParameters { + correlationId?: string | number | Buffer; + messageId?: string | number | Buffer; + properties?: { + [key: string]: any; + }; +} + // @public export interface EventDataBatch { readonly count: number; @@ -225,6 +237,18 @@ export interface LoadBalancingOptions { // @public export const logger: AzureLogger; +// @public +export interface MessageAdapter { + consumeMessage: (message: MessageT) => MessageWithMetadata; + produceMessage: (messageWithMetadata: MessageWithMetadata) => MessageT; +} + +// @public +export interface MessageWithMetadata { + body: Uint8Array; + contentType: string; +} + export { MessagingError } // @public diff --git a/sdk/eventhub/event-hubs/src/eventDataAdapter.ts b/sdk/eventhub/event-hubs/src/eventDataAdapter.ts new file mode 100644 index 000000000000..1813dcebac9b --- /dev/null +++ b/sdk/eventhub/event-hubs/src/eventDataAdapter.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EventData } from "./eventData"; + +/** + * A message with payload and content type fields + * + * This interface is hidden because it is already exported by `@azure/schema-registry-avro` + * + * @hidden + */ +export interface MessageWithMetadata { + /** + * The message's binary data + */ + body: Uint8Array; + /** + * The message's content type + */ + contentType: string; +} + +/** + * A message adapter interface that specifies methods for producing and consuming + * messages with payloads and content type fields. + * + * This interface is hidden because it is already exported by `@azure/schema-registry-avro` + * + * @hidden + */ +export interface MessageAdapter { + /** + * defines how to create a message from a payload and a content type + */ + produceMessage: (messageWithMetadata: MessageWithMetadata) => MessageT; + /** + * defines how to access the payload and the content type of a message + */ + consumeMessage: (message: MessageT) => MessageWithMetadata; +} + +// This type should always be equivalent to Omit, "contentType"> +/** + * Parameters to the `createEventDataAdapter` function that creates an event data adapter. + */ +export interface EventDataAdapterParameters { + /** + * The correlation identifier that allows an + * application to specify a context for the message for the purposes of correlation, for example + * reflecting the MessageId of a message that is being replied to. + */ + correlationId?: string | number | Buffer; + + /** + * The message identifier is an + * application-defined value that uniquely identifies the message and its payload. + * + * Note: Numbers that are not whole integers are not allowed. + */ + messageId?: string | number | Buffer; + + /** + * Set of key value pairs that can be used to set properties specific to user application. + */ + properties?: { + [key: string]: any; + }; +} + +/** + * A function that constructs an event data adapter. That adapter can be used + * with `@azure/schema-registry-avro` to encode and decode body in event data. + * + * @param params - parameters to create the event data + * @returns An event data adapter that can produce and consume event data + */ +export function createEventDataAdapter( + params: EventDataAdapterParameters = {} +): MessageAdapter { + return { + produceMessage: ({ body, contentType }: MessageWithMetadata) => { + return { + ...params, + body, + contentType, + }; + }, + consumeMessage: (message: EventData): MessageWithMetadata => { + const { body, contentType } = message; + if (body === undefined || !(body instanceof Uint8Array)) { + throw new Error("Expected the body field to be defined and have a Uint8Array"); + } + if (contentType === undefined) { + throw new Error("Expected the contentType field to be defined"); + } + return { + body, + contentType, + }; + }, + }; +} diff --git a/sdk/eventhub/event-hubs/src/index.ts b/sdk/eventhub/event-hubs/src/index.ts index 92ca9e5fb5d3..ef65143cbf71 100644 --- a/sdk/eventhub/event-hubs/src/index.ts +++ b/sdk/eventhub/event-hubs/src/index.ts @@ -51,3 +51,5 @@ export { parseEventHubConnectionString, EventHubConnectionStringProperties, } from "./util/connectionStringUtils"; + +export * from "./eventDataAdapter"; diff --git a/sdk/identity/identity/CHANGELOG.md b/sdk/identity/identity/CHANGELOG.md index 607ee22d44b7..dadb6ca5f482 100644 --- a/sdk/identity/identity/CHANGELOG.md +++ b/sdk/identity/identity/CHANGELOG.md @@ -15,6 +15,8 @@ ### Other Changes +- Moved the `@types/stoppable` dependency to the `devDependencies`. + ## 2.0.1 (2021-10-28) ### Features Added diff --git a/sdk/identity/identity/package.json b/sdk/identity/identity/package.json index 454c8dc7c414..7c1f4326f03e 100644 --- a/sdk/identity/identity/package.json +++ b/sdk/identity/identity/package.json @@ -112,7 +112,6 @@ "@azure/msal-common": "^4.5.1", "@azure/msal-node": "^1.3.0", "@azure/msal-browser": "^2.16.0", - "@types/stoppable": "^1.1.0", "events": "^3.0.0", "jws": "^4.0.0", "open": "^8.0.0", @@ -132,6 +131,7 @@ "@types/node": "^12.0.0", "@types/uuid": "^8.0.0", "@types/chai": "^4.1.6", + "@types/stoppable": "^1.1.0", "chai": "^4.2.0", "cross-env": "^7.0.2", "dotenv": "^8.2.0", diff --git a/sdk/schemaregistry/schema-registry-avro/CHANGELOG.md b/sdk/schemaregistry/schema-registry-avro/CHANGELOG.md index f4f295de993e..c78610a6c888 100644 --- a/sdk/schemaregistry/schema-registry-avro/CHANGELOG.md +++ b/sdk/schemaregistry/schema-registry-avro/CHANGELOG.md @@ -4,7 +4,13 @@ ### Features Added +- The serializer APIs have been revamped to work on messages instead of buffers where the payload is the pure encoded-data. The schema ID became part of the content type of that message. This change will improve the experience of using this encoder with the other messaging clients (e.g. Event Hubs, Service Bus, and Event Grid clients). +- `decodeMessageData` now supports decoding using a different but compatible schema + ### Breaking Changes +- The `SchemaRegistryAvroSerializer` class has been renamed to `SchemaRegistryAvroEncoder` +- The `serialize` method has been renamed to `encodeMessageData` and it now returns a message +- The `deserialize` method has been renamed to `decodeMessageData` and it now takes a message as input ### Bugs Fixed diff --git a/sdk/schemaregistry/schema-registry-avro/README.md b/sdk/schemaregistry/schema-registry-avro/README.md index 0cfbf49fd32b..ce80a3b04e8c 100644 --- a/sdk/schemaregistry/schema-registry-avro/README.md +++ b/sdk/schemaregistry/schema-registry-avro/README.md @@ -1,9 +1,9 @@ -# Azure Schema Registry Avro serializer client library for JavaScript +# Azure Schema Registry Avro Encoder client library for JavaScript Azure Schema Registry is a schema repository service hosted by Azure Event Hubs, providing schema storage, versioning, and management. This package provides an -Avro serializer capable of serializing and deserializing payloads containing -Schema Registry schema identifiers and Avro-encoded data. +Avro encoder capable of encoding and decoding payloads containing +Avro-encoded data. Key links: @@ -31,64 +31,68 @@ npm install @azure/schema-registry-avro ## Key concepts -### SchemaRegistryAvroSerializer +### SchemaRegistryAvroEncoder -Provides API to serialize to and deserialize from Avro Binary Encoding plus a -header with schema ID. Uses +Provides API to encode to and decode from Avro Binary Encoding wrapped in a message +with a content type field containing the schema ID. Uses `SchemaRegistryClient` from the [@azure/schema-registry](https://www.npmjs.com/package/@azure/schema-registry) package to get schema IDs from schema definition or vice versa. The provided API has internal cache to avoid calling the schema registry service when possible. -### Message format +### Messages -The same format is used by schema registry serializers across Azure SDK languages. +By default, the encoder will create messages structured as follows: -Messages are encoded as follows: +- `body`: a byte array containing data in the Avro Binary Encoding. Note that it + is NOT Avro Object Container File. The latter includes the schema and creating + it defeats the purpose of using this encoder to move the schema out of the + message payload and into the schema registry. -- 4 bytes: Format Indicator +- `contentType`: a string of the following format `avro/binary+` where + the `avro/binary` part signals that this message has an Avro-encoded payload + and the `` part is the Schema ID the Schema Registry service assigned + to the schema used to encode this payload. - - Currently always zero to indicate format below. - -- 32 bytes: Schema ID - - - UTF-8 hexadecimal representation of GUID. - - 32 hex digits, no hyphens. - - Same format and byte order as string from Schema Registry service. - -- Remaining bytes: Avro payload (in general, format-specific payload) - - - Avro Binary Encoding - - NOT Avro Object Container File, which includes the schema and defeats the - purpose of this serialzer to move the schema out of the message payload and - into the schema registry. +Not all messaging services are supporting the same message structure. To enable +integration with such services, the encoder can act on custom message structures +by setting the `messageAdapter` option in the constructor with a corresponding +message producer and consumer. Azure messaging client libraries export default +adapters for their message types. ## Examples -### Serialize and deserialize +### Encode and decode an `@azure/event-hubs`'s `EventData` ```javascript const { DefaultAzureCredential } = require("@azure/identity"); +import { createEventDataAdapter } from "@azure/event-hubs"; const { SchemaRegistryClient } = require("@azure/schema-registry"); -const { SchemaRegistryAvroSerializer } = require("@azure/schema-registry-avro"); - -const client = new SchemaRegistryClient("", new DefaultAzureCredential()); -const serializer = new SchemaRegistryAvroSerializer(client, { groupName: "" }); +const { SchemaRegistryAvroEncoder } = require("@azure/schema-registry-avro"); + +const client = new SchemaRegistryClient( + "", + new DefaultAzureCredential() +); +const encoder = new SchemaRegistryAvroEncoder(client, { + groupName: "", + messageAdapter: createEventDataAdapter(), +}); // Example Avro schema const schema = JSON.stringify({ type: "record", name: "Rating", namespace: "my.example", - fields: [{ name: "score", type: "int" }] + fields: [{ name: "score", type: "int" }], }); // Example value that matches the Avro schema above const value = { score: 42 }; -// Serialize value to buffer -const buffer = await serializer.serialize(value, schema); +// Encode value to a message +const message = await encoder.encodeMessageData(value, schema); -// Deserialize buffer to value -const deserializedValue = await serializer.deserialize(buffer); +// Decode a message to value +const decodedValue = await encoder.decodeMessageData(message); ``` ## Troubleshooting diff --git a/sdk/schemaregistry/schema-registry-avro/package.json b/sdk/schemaregistry/schema-registry-avro/package.json index 23139ed959ac..d87b883b141a 100644 --- a/sdk/schemaregistry/schema-registry-avro/package.json +++ b/sdk/schemaregistry/schema-registry-avro/package.json @@ -78,7 +78,9 @@ "devDependencies": { "@azure/dev-tool": "^1.0.0", "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "@azure/event-hubs": "^5.7.0-beta.2", "@azure/identity": "^2.0.1", + "@azure/test-utils": "^1.0.0", "@azure-tools/test-recorder": "^1.0.0", "@microsoft/api-extractor": "^7.18.11", "@rollup/plugin-commonjs": "^21.0.1", diff --git a/sdk/schemaregistry/schema-registry-avro/review/schema-registry-avro.api.md b/sdk/schemaregistry/schema-registry-avro/review/schema-registry-avro.api.md index 5dd99b436358..7222e6780ddd 100644 --- a/sdk/schemaregistry/schema-registry-avro/review/schema-registry-avro.api.md +++ b/sdk/schemaregistry/schema-registry-avro/review/schema-registry-avro.api.md @@ -4,21 +4,37 @@ ```ts -/// - import { SchemaRegistry } from '@azure/schema-registry'; // @public -export class SchemaRegistryAvroSerializer { - constructor(client: SchemaRegistry, options?: SchemaRegistryAvroSerializerOptions); - deserialize(input: Buffer | Blob | Uint8Array): Promise; - serialize(value: unknown, schema: string): Promise; +export interface DecodeMessageDataOptions { + schema?: string; +} + +// @public +export interface MessageAdapter { + consumeMessage: (message: MessageT) => MessageWithMetadata; + produceMessage: (messageWithMetadata: MessageWithMetadata) => MessageT; +} + +// @public +export interface MessageWithMetadata { + body: Uint8Array; + contentType: string; +} + +// @public +export class SchemaRegistryAvroEncoder { + constructor(client: SchemaRegistry, options?: SchemaRegistryAvroEncoderOptions); + decodeMessageData(message: MessageT, options?: DecodeMessageDataOptions): Promise; + encodeMessageData(value: unknown, schema: string): Promise; } // @public -export interface SchemaRegistryAvroSerializerOptions { +export interface SchemaRegistryAvroEncoderOptions { autoRegisterSchemas?: boolean; groupName?: string; + messageAdapter?: MessageAdapter; } // (No @packageDocumentation comment for this package) diff --git a/sdk/schemaregistry/schema-registry-avro/sample.env b/sdk/schemaregistry/schema-registry-avro/sample.env index 18e56c11ef2f..15f5a8fc6249 100644 --- a/sdk/schemaregistry/schema-registry-avro/sample.env +++ b/sdk/schemaregistry/schema-registry-avro/sample.env @@ -9,3 +9,12 @@ SCHEMA_REGISTRY_GROUP= AZURE_TENANT_ID= AZURE_CLIENT_ID= AZURE_CLIENT_SECRET= + +# Used in samples that use Event Hubs. Retrieve these values from an Event Hub in the Azure portal. +EVENTHUB_CONNECTION_STRING= +EVENTHUB_NAME= +CONSUMER_GROUP_NAME= + +# Used in samples that use Event Grid. Retrieve these values from an Event Grid topic in the Azure portal +EVENT_GRID_TOPIC_ENDPOINT= +EVENT_GRID_TOPIC_API_KEY= \ No newline at end of file diff --git a/sdk/schemaregistry/schema-registry-avro/samples-dev/schemaRegistryAvroSample.ts b/sdk/schemaregistry/schema-registry-avro/samples-dev/schemaRegistryAvroSample.ts index 8bd4b39f4f80..e54248ddcf39 100644 --- a/sdk/schemaregistry/schema-registry-avro/samples-dev/schemaRegistryAvroSample.ts +++ b/sdk/schemaregistry/schema-registry-avro/samples-dev/schemaRegistryAvroSample.ts @@ -2,19 +2,22 @@ // Licensed under the MIT License. /** - * @summary Demonstrates the use of SchemaRegistryAvroSerializer to serialize and deserialize using schema from Schema Registry. + * @summary Demonstrates the use of SchemaRegistryAvroEncoder to create messages with avro-encoded payload using schema from Schema Registry. */ import { DefaultAzureCredential } from "@azure/identity"; import { SchemaRegistryClient, SchemaDescription } from "@azure/schema-registry"; -import { SchemaRegistryAvroSerializer } from "@azure/schema-registry-avro"; +import { SchemaRegistryAvroEncoder } from "@azure/schema-registry-avro"; // Load the .env file if it exists import * as dotenv from "dotenv"; dotenv.config(); -// Set these environment variables or edit the following values -const endpoint = process.env["SCHEMA_REGISTRY_ENDPOINT"] || ""; +// The fully qualified namespace for schema registry +const schemaRegistryFullyQualifiedNamespace = + process.env["SCHEMA_REGISTRY_ENDPOINT"] || ""; + +// The schema group to use for schema registeration or lookup const groupName = process.env["SCHEMA_REGISTRY_GROUP"] || "AzureSdkSampleGroup"; // Sample Avro Schema for user with first and last names @@ -52,26 +55,29 @@ const schemaDescription: SchemaDescription = { export async function main() { // Create a new client - const client = new SchemaRegistryClient(endpoint, new DefaultAzureCredential()); + const client = new SchemaRegistryClient( + schemaRegistryFullyQualifiedNamespace, + new DefaultAzureCredential() + ); // Register the schema. This would generally have been done somewhere else. - // You can also skip this step and let serialize automatically register schemas - // using autoRegisterSchemas=true, but that is NOT recommended in production. + // You can also skip this step and let `encodeMessageData` automatically register + // schemas using autoRegisterSchemas=true, but that is NOT recommended in production. await client.registerSchema(schemaDescription); - // Create a new serializer backed by the client - const serializer = new SchemaRegistryAvroSerializer(client, { groupName }); + // Create a new encoder backed by the client + const encoder = new SchemaRegistryAvroEncoder(client, { groupName }); - // serialize an object that matches the schema + // encode an object that matches the schema and put it in a message const value: User = { firstName: "Jane", lastName: "Doe" }; - const buffer = await serializer.serialize(value, schema); - console.log("Serialized:"); - console.log(buffer); + const message = await encoder.encodeMessageData(value, schema); + console.log("Created message:"); + console.log(JSON.stringify(message)); - // deserialize the result back to an object - const deserializedValue = (await serializer.deserialize(buffer)) as User; - console.log("Deserialized:"); - console.log(`${deserializedValue.firstName} ${deserializedValue.lastName}`); + // decode the message back to an object + const decodedObject = await encoder.decodeMessageData(message); + console.log("Decoded object:"); + console.log(JSON.stringify(decodedObject as User)); } main().catch((err) => { diff --git a/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsBufferedProducerClient.ts b/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsBufferedProducerClient.ts new file mode 100644 index 000000000000..923e7cd4d932 --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsBufferedProducerClient.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @summary Demonstrates the use of SchemaRegistryAvroEncoder to create messages with avro-encoded payload using schema from Schema Registry and send them to an Event Hub using the EventHub Buffered Producer Client. + */ + +import { DefaultAzureCredential } from "@azure/identity"; +import { SchemaRegistryClient, SchemaDescription } from "@azure/schema-registry"; +import { SchemaRegistryAvroEncoder } from "@azure/schema-registry-avro"; +import { EventHubBufferedProducerClient, createEventDataAdapter } from "@azure/event-hubs"; + +// Load the .env file if it exists +import * as dotenv from "dotenv"; +dotenv.config(); + +// The fully qualified namespace for schema registry +const schemaRegistryFullyQualifiedNamespace = + process.env["SCHEMA_REGISTRY_ENDPOINT"] || ""; + +// The schema group to use for schema registeration or lookup +const groupName = process.env["SCHEMA_REGISTRY_GROUP"] || "AzureSdkSampleGroup"; + +// The connection string for Event Hubs +const eventHubsConnectionString = process.env["EVENTHUB_CONNECTION_STRING"] || ""; + +// Sample Avro Schema for user with first and last names +const schemaObject = { + type: "record", + name: "User", + namespace: "com.azure.schemaregistry.samples", + fields: [ + { + name: "firstName", + type: "string", + }, + { + name: "lastName", + type: "string", + }, + ], +}; + +// Matching TypeScript interface for schema +interface User { + firstName: string; + lastName: string; +} + +const schema = JSON.stringify(schemaObject); + +// Description of the schema for registration +const schemaDescription: SchemaDescription = { + name: `${schemaObject.namespace}.${schemaObject.name}`, + groupName, + format: "Avro", + definition: schema, +}; + +async function handleError(): Promise { + console.log("An error occured when sending a message"); +} + +export async function main() { + // Create a new client + const schemaRegistryClient = new SchemaRegistryClient( + schemaRegistryFullyQualifiedNamespace, + new DefaultAzureCredential() + ); + + // Register the schema. This would generally have been done somewhere else. + // You can also skip this step and let `encodeMessageData` automatically register + // schemas using autoRegisterSchemas=true, but that is NOT recommended in production. + await schemaRegistryClient.registerSchema(schemaDescription); + + // Create a new encoder backed by the client + const encoder = new SchemaRegistryAvroEncoder(schemaRegistryClient, { + groupName, + messageAdapter: createEventDataAdapter(), + }); + + const eventHubsBufferedProducerClient = new EventHubBufferedProducerClient( + eventHubsConnectionString, + { + onSendEventsErrorHandler: handleError, + } + ); + + // encode an object that matches the schema + const value: User = { firstName: "Jane", lastName: "Doe" }; + const message = await encoder.encodeMessageData(value, schema); + console.log("Created message:"); + console.log(message); + + await eventHubsBufferedProducerClient.enqueueEvent(message); + console.log(`Message was added to the queue and is about to be sent`); + + // Wait for a bit before cleaning up the sample + setTimeout(async () => { + await eventHubsBufferedProducerClient.close({ flush: true }); + console.log(`Exiting sample`); + }, 30 * 1000); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsConsumerClient.ts b/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsConsumerClient.ts new file mode 100644 index 000000000000..be7f83f5c682 --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsConsumerClient.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @summary Demonstrates the use of SchemaRegistryAvroEncoder to decode messages with avro-encoded payload received from the Event Hub Consumer Client. + */ + +import { DefaultAzureCredential } from "@azure/identity"; +import { SchemaRegistryClient, SchemaDescription } from "@azure/schema-registry"; +import { SchemaRegistryAvroEncoder } from "@azure/schema-registry-avro"; +import { + EventHubConsumerClient, + earliestEventPosition, + createEventDataAdapter, +} from "@azure/event-hubs"; + +// Load the .env file if it exists +import * as dotenv from "dotenv"; +dotenv.config(); + +// The fully qualified namespace for schema registry +const schemaRegistryFullyQualifiedNamespace = + process.env["SCHEMA_REGISTRY_ENDPOINT"] || ""; + +// The schema group to use for schema registeration or lookup +const groupName = process.env["SCHEMA_REGISTRY_GROUP"] || "AzureSdkSampleGroup"; + +// The connection string for Event Hubs +const eventHubsConnectionString = process.env["EVENTHUB_CONNECTION_STRING"] || ""; + +// The name of Event Hub the client will connect to +const eventHubName = process.env["EVENTHUB_NAME"] || ""; + +// The name of the Event Hub consumer group from which you want to process events +const consumerGroup = process.env["CONSUMER_GROUP_NAME"] || ""; + +// Sample Avro Schema for user with first and last names +const schemaObject = { + type: "record", + name: "User", + namespace: "com.azure.schemaregistry.samples", + fields: [ + { + name: "firstName", + type: "string", + }, + { + name: "lastName", + type: "string", + }, + ], +}; + +const schema = JSON.stringify(schemaObject); + +// Description of the schema for registration +const schemaDescription: SchemaDescription = { + name: `${schemaObject.namespace}.${schemaObject.name}`, + groupName, + format: "Avro", + definition: schema, +}; + +export async function main() { + // Create a new client + const schemaRegistryClient = new SchemaRegistryClient( + schemaRegistryFullyQualifiedNamespace, + new DefaultAzureCredential() + ); + + // Register the schema. This would generally have been done somewhere else. + // You can also skip this step and let `encodeMessageData` automatically register + // schemas using autoRegisterSchemas=true, but that is NOT recommended in production. + await schemaRegistryClient.registerSchema(schemaDescription); + + // Create a new encoder backed by the client + const encoder = new SchemaRegistryAvroEncoder(schemaRegistryClient, { + groupName, + messageAdapter: createEventDataAdapter(), + }); + + const eventHubConsumerClient = new EventHubConsumerClient( + consumerGroup, + eventHubsConnectionString, + eventHubName + ); + + const subscription = eventHubConsumerClient.subscribe( + { + // The callback where you add your code to process incoming events + processEvents: async (events, context) => { + // Note: It is possible for `events` to be an empty array. + // This can happen if there were no new events to receive + // in the `maxWaitTimeInSeconds`, which is defaulted to + // 60 seconds. + // The `maxWaitTimeInSeconds` can be changed by setting + // it in the `options` passed to `subscribe()`. + for (const event of events) { + console.log( + `Received event: '${JSON.stringify(event)}' from partition: '${ + context.partitionId + }' and consumer group: '${context.consumerGroup}'` + ); + if (event.contentType !== undefined && event.body) { + const contentTypeParts = event.contentType.split("+"); + if (contentTypeParts[0] === "avro/binary") { + const decodedEvent = await encoder.decodeMessageData(event); + console.log(`Decoded message: '${JSON.stringify(decodedEvent)}'`); + } + } + } + }, + processError: async (err, context) => { + console.log(`Error on partition "${context.partitionId}": ${err}`); + }, + }, + { startPosition: earliestEventPosition } + ); + + // Wait for a bit before cleaning up the sample + setTimeout(async () => { + await subscription.close(); + await eventHubConsumerClient.close(); + console.log(`Exiting sample`); + }, 30 * 1000); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsProducerClient.ts b/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsProducerClient.ts new file mode 100644 index 000000000000..37f183236d1d --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/samples-dev/withEventHubsProducerClient.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @summary Demonstrates the use of SchemaRegistryAvroEncoder to create messages with avro-encoded payload using schema from Schema Registry and send them to an Event Hub using the EventHub Producer Client. + */ + +import { DefaultAzureCredential } from "@azure/identity"; +import { SchemaRegistryClient, SchemaDescription } from "@azure/schema-registry"; +import { SchemaRegistryAvroEncoder } from "@azure/schema-registry-avro"; +import { EventHubProducerClient, createEventDataAdapter } from "@azure/event-hubs"; + +// Load the .env file if it exists +import * as dotenv from "dotenv"; +dotenv.config(); + +// The fully qualified namespace for schema registry +const schemaRegistryFullyQualifiedNamespace = + process.env["SCHEMA_REGISTRY_ENDPOINT"] || ""; + +// The schema group to use for schema registeration or lookup +const groupName = process.env["SCHEMA_REGISTRY_GROUP"] || "AzureSdkSampleGroup"; + +// The connection string for Event Hubs +const eventHubsConnectionString = process.env["EVENTHUB_CONNECTION_STRING"] || ""; + +// The name of Event Hub the client will connect to +const eventHubName = process.env["EVENTHUB_NAME"] || ""; + +// Sample Avro Schema for user with first and last names +const schemaObject = { + type: "record", + name: "User", + namespace: "com.azure.schemaregistry.samples", + fields: [ + { + name: "firstName", + type: "string", + }, + { + name: "lastName", + type: "string", + }, + ], +}; + +// Matching TypeScript interface for schema +interface User { + firstName: string; + lastName: string; +} + +const schema = JSON.stringify(schemaObject); + +// Description of the schema for registration +const schemaDescription: SchemaDescription = { + name: `${schemaObject.namespace}.${schemaObject.name}`, + groupName, + format: "Avro", + definition: schema, +}; + +export async function main() { + // Create a new client + const schemaRegistryClient = new SchemaRegistryClient( + schemaRegistryFullyQualifiedNamespace, + new DefaultAzureCredential() + ); + + // Register the schema. This would generally have been done somewhere else. + // You can also skip this step and let `encodeMessageData` automatically register + // schemas using autoRegisterSchemas=true, but that is NOT recommended in production. + await schemaRegistryClient.registerSchema(schemaDescription); + + // Create a new encoder backed by the client + const encoder = new SchemaRegistryAvroEncoder(schemaRegistryClient, { + groupName, + messageAdapter: createEventDataAdapter(), + }); + + const eventHubsProducerClient = new EventHubProducerClient( + eventHubsConnectionString, + eventHubName + ); + + // encode an object that matches the schema + const value: User = { firstName: "Joe", lastName: "Doe" }; + const message = await encoder.encodeMessageData(value, schema); + console.log("Created message:"); + console.log(message); + + const eventsToSend = [message]; + + // By not specifying a partition ID or a partition key we allow the server to choose + // which partition will accept this message. + // + // This pattern works well if the consumers of your events do not have any particular + // requirements about the ordering of batches against other batches or if you don't care + // which messages are assigned to which partition. + // + // If you would like more control you can pass either a `partitionKey` or a `partitionId` + // into the createBatch() `options` parameter which will allow you full control over the + // destination. + const batchOptions = { + // The maxSizeInBytes lets you manually control the size of the batch. + // if this is not set we will get the maximum batch size from Event Hubs. + // + // For this sample you can change the batch size to see how different parts + // of the sample handle batching. In production we recommend using the default + // and not specifying a maximum size. + // + // maxSizeInBytes: 200 + }; + + let batch = await eventHubsProducerClient.createBatch(batchOptions); + + let numEventsSent = 0; + + // add events to our batch + let i = 0; + + while (i < eventsToSend.length) { + // messages can fail to be added to the batch if they exceed the maximum size configured for + // the EventHub. + const isAdded = batch.tryAdd(eventsToSend[i]); + + if (isAdded) { + console.log(`Added a message with index ${i} to the batch`); + ++i; + continue; + } + + if (batch.count === 0) { + // If we can't add it and the batch is empty that means the message we're trying to send + // is too large, even when it would be the _only_ message in the batch. + // + // At this point you'll need to decide if you're okay with skipping this message entirely + // or find some way to shrink it. + console.log(`Message was too large and can't be sent until it's made smaller. Skipping...`); + ++i; + continue; + } + + // otherwise this just signals a good spot to send our batch + console.log(`Batch is full - sending ${batch.count} messages as a single batch.`); + await eventHubsProducerClient.sendBatch(batch); + numEventsSent += batch.count; + + // and create a new one to house the next set of messages + batch = await eventHubsProducerClient.createBatch(batchOptions); + } + + // send any remaining messages, if any. + if (batch.count > 0) { + console.log(`Sending remaining ${batch.count} messages as a single batch.`); + await eventHubsProducerClient.sendBatch(batch); + numEventsSent += batch.count; + } + + console.log(`Sent ${numEventsSent} events`); + + if (numEventsSent !== eventsToSend.length) { + throw new Error(`Not all messages were sent (${numEventsSent}/${eventsToSend.length})`); + } + + // Wait for a bit before cleaning up the sample + setTimeout(async () => { + await eventHubsProducerClient.close(); + console.log(`Exiting sample`); + }, 3 * 1000); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/schemaregistry/schema-registry-avro/src/index.ts b/sdk/schemaregistry/schema-registry-avro/src/index.ts index 54cefa3b5d8e..5c6530a707e3 100644 --- a/sdk/schemaregistry/schema-registry-avro/src/index.ts +++ b/sdk/schemaregistry/schema-registry-avro/src/index.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export { - SchemaRegistryAvroSerializer, - SchemaRegistryAvroSerializerOptions, -} from "./schemaRegistryAvroSerializer"; +export { SchemaRegistryAvroEncoder } from "./schemaRegistryAvroEncoder"; + +export * from "./models"; diff --git a/sdk/schemaregistry/schema-registry-avro/src/models.ts b/sdk/schemaregistry/schema-registry-avro/src/models.ts new file mode 100644 index 000000000000..1482e7ef92df --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/src/models.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * A message that contains binary data and a content type. + */ +export interface MessageWithMetadata { + /** + * The message's binary data + */ + body: Uint8Array; + /** + * The message's content type + */ + contentType: string; +} + +/** + * MessageAdapter is an interface that converts to/from a concrete message type + * to a message with metadata + */ +export interface MessageAdapter { + /** + * defines how to create a message from a payload and a content type + */ + produceMessage: (messageWithMetadata: MessageWithMetadata) => MessageT; + /** + * defines how to access the payload and the content type of a message + */ + consumeMessage: (message: MessageT) => MessageWithMetadata; +} + +/** + * Options for Schema + */ +export interface SchemaRegistryAvroEncoderOptions { + /** + * When true, register new schemas passed to encodeMessageData. Otherwise, and by + * default, fail if schema has not already been registered. + * + * Automatic schema registration is NOT recommended for production scenarios. + */ + autoRegisterSchemas?: boolean; + /** + * The group name to be used when registering/looking up a schema. Must be specified + * if `encodeMessageData` will be called. + */ + groupName?: string; + /** + * Message Adapter enables the encoder to produce and consume custom messages. + */ + messageAdapter?: MessageAdapter; +} + +/** + * The options to the decodeMessageData method. + */ +export interface DecodeMessageDataOptions { + /** + * The schema to be used for decoding. + */ + schema?: string; +} diff --git a/sdk/schemaregistry/schema-registry-avro/src/schemaRegistryAvroEncoder.ts b/sdk/schemaregistry/schema-registry-avro/src/schemaRegistryAvroEncoder.ts new file mode 100644 index 000000000000..1ac215029a63 --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/src/schemaRegistryAvroEncoder.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as avro from "avsc"; +import { + DecodeMessageDataOptions, + MessageAdapter, + MessageWithMetadata, + SchemaRegistryAvroEncoderOptions, +} from "./models"; +import { SchemaDescription, SchemaRegistry } from "@azure/schema-registry"; +import { isMessageWithMetadata } from "./utility"; + +interface CacheEntry { + /** Schema ID */ + id: string; + + /** avsc-specific representation for schema */ + type: avro.Type; +} + +const avroMimeType = "avro/binary"; + +/** + * Avro encoder that obtains schemas from a schema registry and does not + * pack schemas into its payloads. + */ +export class SchemaRegistryAvroEncoder { + /** + * Creates a new encoder. + * + * @param client - Schema Registry where schemas are registered and obtained. + * Usually this is a SchemaRegistryClient instance. + */ + constructor(client: SchemaRegistry, options?: SchemaRegistryAvroEncoderOptions) { + this.registry = client; + this.schemaGroup = options?.groupName; + this.autoRegisterSchemas = options?.autoRegisterSchemas ?? false; + this.messageAdapter = options?.messageAdapter; + } + + private readonly schemaGroup?: string; + private readonly registry: SchemaRegistry; + private readonly autoRegisterSchemas: boolean; + private readonly messageAdapter?: MessageAdapter; + + // REVIEW: signature. + // + // - Should we wrap all errors thrown by avsc to avoid having our exception // + // contract being tied to its implementation details? + /** + * encodes the value parameter according to the input schema and creates a message + * with the encoded data. + * + * @param value - The value to encodeMessageData. + * @param schema - The Avro schema to use. + * @returns A new message with the encoded value. The structure of message is + * constrolled by the message factory option. + */ + async encodeMessageData(value: unknown, schema: string): Promise { + const entry = await this.getSchemaByDefinition(schema); + const buffer = entry.type.toBuffer(value); + const payload = new Uint8Array( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT + ); + const contentType = `${avroMimeType}+${entry.id}`; + return this.messageAdapter + ? this.messageAdapter.produceMessage({ + contentType, + body: payload, + }) + : /** + * If no message consumer was provided, then a MessageWithMetadata will be + * returned. This should work because the MessageT type parameter defaults + * to MessageWithMetadata. + */ + ({ + body: payload, + contentType: contentType, + } as unknown as MessageT); + } + + /** + * Decodes the payload of the message using the schema ID in the content type + * field if no schema was provided. + * + * @param message - The message with the payload to be decoded. + * @param options - Decoding options. + * @returns The decoded value. + */ + async decodeMessageData( + message: MessageT, + options: DecodeMessageDataOptions = {} + ): Promise { + const { schema: readerSchema } = options; + const { body, contentType } = getPayloadAndContent(message, this.messageAdapter); + const buffer = Buffer.from(body); + const writerSchemaId = getSchemaId(contentType); + const writerSchema = await this.getSchema(writerSchemaId); + if (readerSchema) { + const avscReaderSchema = this.getAvroTypeForSchema(readerSchema); + const resolver = avscReaderSchema.createResolver(writerSchema.type); + return avscReaderSchema.fromBuffer(buffer, resolver, true); + } else { + return writerSchema.type.fromBuffer(buffer); + } + } + + private readonly cacheBySchemaDefinition = new Map(); + private readonly cacheById = new Map(); + + private async getSchema(schemaId: string): Promise { + const cached = this.cacheById.get(schemaId); + if (cached) { + return cached; + } + + const schemaResponse = await this.registry.getSchema(schemaId); + if (!schemaResponse) { + throw new Error(`Schema with ID '${schemaId}' not found.`); + } + + if (!schemaResponse.properties.format.match(/^avro$/i)) { + throw new Error( + `Schema with ID '${schemaResponse.properties.id}' has format '${schemaResponse.properties.format}', not 'avro'.` + ); + } + + const avroType = this.getAvroTypeForSchema(schemaResponse.definition); + return this.cache(schemaId, schemaResponse.definition, avroType); + } + + private async getSchemaByDefinition(schema: string): Promise { + const cached = this.cacheBySchemaDefinition.get(schema); + if (cached) { + return cached; + } + + const avroType = this.getAvroTypeForSchema(schema); + if (!avroType.name) { + throw new Error("Schema must have a name."); + } + + if (!this.schemaGroup) { + throw new Error( + "Schema group must have been specified in the constructor options when the client was created in order to encode." + ); + } + + const description: SchemaDescription = { + groupName: this.schemaGroup, + name: avroType.name, + format: "Avro", + definition: schema, + }; + + let id: string; + if (this.autoRegisterSchemas) { + id = (await this.registry.registerSchema(description)).id; + } else { + try { + id = (await this.registry.getSchemaProperties(description)).id; + } catch (e) { + if (e.statusCode === 404) { + throw new Error( + `Schema '${description.name}' not found in registry group '${description.groupName}', or not found to have matching definition.` + ); + } else { + throw e; + } + } + } + + return this.cache(id, schema, avroType); + } + + private cache(id: string, schema: string, type: avro.Type): CacheEntry { + const entry = { id, type }; + this.cacheBySchemaDefinition.set(schema, entry); + this.cacheById.set(id, entry); + return entry; + } + + private getAvroTypeForSchema(schema: string): avro.Type { + return avro.Type.forSchema(JSON.parse(schema), { omitRecordMethods: true }); + } +} + +function getSchemaId(contentType: string): string { + const contentTypeParts = contentType.split("+"); + if (contentTypeParts.length !== 2) { + throw new Error("Content type was not in the expected format of MIME type + schema ID"); + } + if (contentTypeParts[0] !== avroMimeType) { + throw new Error( + `Received content of type ${contentTypeParts[0]} but an avro encoder may only be used on content that is of '${avroMimeType}' type` + ); + } + return contentTypeParts[1]; +} + +function getPayloadAndContent( + message: MessageT, + messageAdapter?: MessageAdapter +): MessageWithMetadata { + const messageConsumer = messageAdapter?.consumeMessage; + if (messageConsumer) { + return messageConsumer(message); + } else if (isMessageWithMetadata(message)) { + return message; + } else { + throw new Error( + `Either the messageConsumer option should be defined or the message should have body and contentType fields` + ); + } +} diff --git a/sdk/schemaregistry/schema-registry-avro/src/schemaRegistryAvroSerializer.ts b/sdk/schemaregistry/schema-registry-avro/src/schemaRegistryAvroSerializer.ts deleted file mode 100644 index 02af75187e13..000000000000 --- a/sdk/schemaregistry/schema-registry-avro/src/schemaRegistryAvroSerializer.ts +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { SchemaDescription, SchemaRegistry } from "@azure/schema-registry"; -import * as avro from "avsc"; -import { toUint8Array } from "./utils/buffer"; - -// REVIEW: This should go in to a shared doc somewhere that all of the different -// language serializer's docs can reference. -// -// Wire format -// ----------- -// -// This is a standard meant to be reused across schema registry serializers for -// different format. We only have an avro serializer at this point so picking -// apart this format is inlined here, but handling could be extracted and shared -// between serializers of different formats in the future. -// -// - [4 bytes: Format Indicator] -// - Currently always zero to indicate format below. -// -// - [32 bytes: Schema ID] -// - UTF-8 hexadecimal representation of GUID. -// - 32 hex digits, no hyphens. -// - Same format and byte order as string from Schema Registry service. -// -// - [Remaining bytes: Avro payload (in general, format-specific payload)] -// - Avro Binary Encoding -// - NOT Avro Object Container File, which includes the schema and defeats -// the purpose of this serializer to move the schema out of the message -// payload and into the schema registry. -// -const FORMAT_INDICATOR = 0; -const SCHEMA_ID_OFFSET = 4; -const SCHEMA_ID_LENGTH = 32; -const PAYLOAD_OFFSET = 36; - -interface CacheEntry { - /** Schema ID */ - id: string; - - /** avsc-specific representation for schema */ - type: avro.Type; -} - -/** - * Options for Schema - */ -export interface SchemaRegistryAvroSerializerOptions { - /** - * When true, register new schemas passed to serialize. Otherwise, and by - * default, fail if schema has not already been registered. - * - * Automatic schema registration is NOT recommended for production scenarios. - */ - autoRegisterSchemas?: boolean; - /** - * The group name to be used when registering/looking up a schema. Must be specified - * if you will be calling `serialize`. - */ - groupName?: string; -} - -/** - * Avro serializer that obtains schemas from a schema registry and does not - * pack schemas into its payloads. - */ -export class SchemaRegistryAvroSerializer { - /** - * Creates a new serializer. - * - * @param client - Schema Registry where schemas are registered and obtained. - * Usually this is a SchemaRegistryClient instance. - */ - constructor(client: SchemaRegistry, options?: SchemaRegistryAvroSerializerOptions) { - this.registry = client; - this.schemaGroup = options?.groupName; - this.autoRegisterSchemas = options?.autoRegisterSchemas ?? false; - } - - private readonly schemaGroup?: string; - private readonly registry: SchemaRegistry; - private readonly autoRegisterSchemas: boolean; - - // REVIEW: signature. - // - // - Better to serialize into a stream? I aborted that for now as I wanted to - // do the simplest thing that could possibly work first to make sure there - // were no blockers in our dependencies. I also wanted to get feedback on - // what the API shape should be before diving into that. - // - // - This type should ultimately be able to implement a core ObjectSerializer - // interface. Do we know what that would look like? Maybe it takes `any` as - // the format-specific schema/type info? Or does it always take a - // format-specific schema string? - // - // The C#/Java approach of passing Type and assuming every serializer can - // get its schema by reflecting on the type does not work for JavaScript. We - // need to support arbitrary objects that match a schema. - // - // Maybe each format expects a different property on this arg so that you - // could at least pass enough info for multiple formats, and then your call - // to ObjectSerializer is at least not tied to a single format? - // - // - Should we wrap all errors thrown by avsc to avoid having our exception // - // contract being tied to its implementation details? - /** - * Serializes a value into a buffer. - * - * @param value - The value to serialize. - * @param schema - The Avro schema to use. - * @returns A new buffer with the serialized value - */ - async serialize(value: unknown, schema: string): Promise { - const entry = await this.getSchemaByDefinition(schema); - const payload = entry.type.toBuffer(value); - const buffer = Buffer.alloc(PAYLOAD_OFFSET + payload.length); - - buffer.writeUInt32BE(FORMAT_INDICATOR, 0); - buffer.write(entry.id, SCHEMA_ID_OFFSET, SCHEMA_ID_LENGTH, "utf-8"); - payload.copy(buffer, PAYLOAD_OFFSET); - return new Uint8Array( - buffer.buffer, - buffer.byteOffset, - buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT - ); - } - - // REVIEW: signature. See serialize and s/serialize into/deserialize from/. - /** - * Deserializes a value from a buffer. - * - * @param buffer - The buffer with the serialized value. - * @returns The deserialized value. - */ - async deserialize(input: Buffer | Blob | Uint8Array): Promise { - const arr8 = await toUint8Array(input); - const buffer = Buffer.isBuffer(arr8) ? arr8 : Buffer.from(arr8); - if (buffer.length < PAYLOAD_OFFSET) { - throw new RangeError("Buffer is too small to have the correct format."); - } - - const format = buffer.readUInt32BE(0); - if (format !== FORMAT_INDICATOR) { - throw new TypeError(`Buffer has unknown format indicator: 0x${format.toString(16)}.`); - } - - const schemaIdBuffer = buffer.slice(SCHEMA_ID_OFFSET, PAYLOAD_OFFSET); - const schemaId = schemaIdBuffer.toString("utf-8"); - const schema = await this.getSchema(schemaId); - const payloadBuffer = buffer.slice(PAYLOAD_OFFSET); - - return schema.type.fromBuffer(payloadBuffer); - } - - private readonly cacheBySchemaDefinition = new Map(); - private readonly cacheById = new Map(); - - private async getSchema(schemaId: string): Promise { - const cached = this.cacheById.get(schemaId); - if (cached) { - return cached; - } - - const schemaResponse = await this.registry.getSchema(schemaId); - if (!schemaResponse) { - throw new Error(`Schema with ID '${schemaId}' not found.`); - } - - if (!schemaResponse.properties.format.match(/^avro$/i)) { - throw new Error( - `Schema with ID '${schemaResponse.properties.id}' has format '${schemaResponse.properties.format}', not 'avro'.` - ); - } - - const avroType = this.getAvroTypeForSchema(schemaResponse.definition); - return this.cache(schemaId, schemaResponse.definition, avroType); - } - - private async getSchemaByDefinition(schema: string): Promise { - const cached = this.cacheBySchemaDefinition.get(schema); - if (cached) { - return cached; - } - - const avroType = this.getAvroTypeForSchema(schema); - if (!avroType.name) { - throw new Error("Schema must have a name."); - } - - if (!this.schemaGroup) { - throw new Error( - "Schema group must have been specified in the constructor options when the client was created in order to serialize." - ); - } - - const description: SchemaDescription = { - groupName: this.schemaGroup, - name: avroType.name, - format: "Avro", - definition: schema, - }; - - let id: string; - if (this.autoRegisterSchemas) { - id = (await this.registry.registerSchema(description)).id; - } else { - try { - id = (await this.registry.getSchemaProperties(description)).id; - } catch (e) { - if (e.statusCode === 404) { - throw new Error( - `Schema '${description.name}' not found in registry group '${description.groupName}', or not found to have matching definition.` - ); - } else { - throw e; - } - } - } - - return this.cache(id, schema, avroType); - } - - private cache(id: string, schema: string, type: avro.Type): CacheEntry { - const entry = { id, type }; - this.cacheBySchemaDefinition.set(schema, entry); - this.cacheById.set(id, entry); - return entry; - } - - private getAvroTypeForSchema(schema: string): avro.Type { - return avro.Type.forSchema(JSON.parse(schema), { omitRecordMethods: true }); - } -} diff --git a/sdk/schemaregistry/schema-registry-avro/src/utility.ts b/sdk/schemaregistry/schema-registry-avro/src/utility.ts new file mode 100644 index 000000000000..9578eeb39b6b --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/src/utility.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { MessageWithMetadata } from "./models"; + +export function isMessageWithMetadata(message: unknown): message is MessageWithMetadata { + const castMessage = message as MessageWithMetadata; + return castMessage.body !== undefined && castMessage.contentType !== undefined; +} diff --git a/sdk/schemaregistry/schema-registry-avro/src/utils/buffer.browser.ts b/sdk/schemaregistry/schema-registry-avro/src/utils/buffer.browser.ts deleted file mode 100644 index 9d1a069df24f..000000000000 --- a/sdk/schemaregistry/schema-registry-avro/src/utils/buffer.browser.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -async function blobToArrayBuffer(blob: Blob): Promise { - if ("arrayBuffer" in blob) { - return blob.arrayBuffer(); - } - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as ArrayBuffer); - reader.onerror = () => reject; - reader.readAsArrayBuffer(blob); - }); -} - -/** - * @param input - Input to `deserialize`. - * @returns Promise which completes with the input data as a Uint8Array. - */ -export async function toUint8Array(input: Uint8Array | Buffer | Blob): Promise { - // If this is not a Uint8Array, assume it's a blob and retrieve an ArrayBuffer from the blob. - if ((input as any).byteLength === undefined) { - return new Uint8Array(await blobToArrayBuffer(input as Blob)); - } - return input as Uint8Array; -} diff --git a/sdk/schemaregistry/schema-registry-avro/src/utils/buffer.ts b/sdk/schemaregistry/schema-registry-avro/src/utils/buffer.ts deleted file mode 100644 index f7a07c5d8693..000000000000 --- a/sdk/schemaregistry/schema-registry-avro/src/utils/buffer.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @param input - Input to `deserialize`. - * @returns Promise which completes with the input data as a Uint8Array. - */ -export async function toUint8Array(input: Uint8Array | Buffer | Blob): Promise { - if ((input as any).byteLength === undefined) { - throw TypeError("Blob is unsupported in node."); - } - return input as Uint8Array; -} diff --git a/sdk/schemaregistry/schema-registry-avro/test/messageAdapter.spec.ts b/sdk/schemaregistry/schema-registry-avro/test/messageAdapter.spec.ts new file mode 100644 index 000000000000..b24dcec87a81 --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/test/messageAdapter.spec.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EventData, createEventDataAdapter, EventDataAdapterParameters } from "@azure/event-hubs"; +import { matrix } from "@azure/test-utils"; +import { MessageAdapter } from "../src/models"; +import { assert } from "chai"; + +/** + * A type predicate to check whether two record types have the same keys + */ +type AssertEqualKeys, T2 extends Record> = [ + keyof T1 extends keyof T2 ? 1 : 0, + keyof T2 extends keyof T1 ? 1 : 0 +] extends [1, 1] + ? true + : false; + +function isMessageAdapter(obj: any): obj is MessageAdapter { + return typeof obj.produceMessage === "function" && typeof obj.consumeMessage === "function"; +} + +/** + * some tests consume messages with well-formed Uint8Array payload so this variable + * is used to construct those. + */ +const dummyUint8Array = Uint8Array.from([0]); + +/** + * An interface to group different bits needed by the tests for each adapter + * factory + */ +interface AdapterTestInfo { + adapterFactory: MessageAdapter; + nonUint8ArrayMessage: T; + adapterFactoryName: string; +} + +const eventDataAdapterTestInfo: AdapterTestInfo = { + adapterFactory: createEventDataAdapter(), + nonUint8ArrayMessage: { + body: "", + contentType: "", + }, + adapterFactoryName: createEventDataAdapter.name, +}; + +describe("Message Adapters", function () { + describe("Input types for message adapter factories are sound", function () { + it("EventDataAdapterParameters", function () { + const areEqual: AssertEqualKeys< + EventDataAdapterParameters, + Omit + > = true; + assert.isTrue( + areEqual, + 'EventDataAdapterParameters should have the same shape as Omit.' + ); + }); + }); + matrix([[eventDataAdapterTestInfo]] as const, async (adapterTestInfo: AdapterTestInfo) => { + describe(adapterTestInfo.adapterFactoryName, function () { + const adapter = adapterTestInfo.adapterFactory; + it("implements MessageAdapter", async () => { + assert.isTrue(isMessageAdapter(adapter), `should create a valid MessageAdapter`); + }); + it("consumeMessage rejects non-Uint8Array body", async () => { + assert.throws( + () => adapter.consumeMessage(adapterTestInfo.nonUint8ArrayMessage), + /Expected the body field to be defined and have a Uint8Array/ + ); + }); + it("consumeMessage rejects messages with no contentType", async () => { + assert.throws( + () => + adapter.consumeMessage({ + body: dummyUint8Array, + }), + /Expected the contentType field to be defined/ + ); + }); + }); + }); +}); diff --git a/sdk/schemaregistry/schema-registry-avro/test/schemaRegistryAvroEncoder.spec.ts b/sdk/schemaregistry/schema-registry-avro/test/schemaRegistryAvroEncoder.spec.ts new file mode 100644 index 000000000000..4deb210f1278 --- /dev/null +++ b/sdk/schemaregistry/schema-registry-avro/test/schemaRegistryAvroEncoder.spec.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert, use as chaiUse } from "chai"; +import { createTestEncoder, registerTestSchema } from "./utils/mockedEncoder"; +import { testAvroType, testGroup, testSchema, testSchemaIds, testValue } from "./utils/dummies"; +import chaiPromises from "chai-as-promised"; +import { createTestRegistry } from "./utils/mockedRegistryClient"; + +chaiUse(chaiPromises); + +describe("SchemaRegistryAvroEncoder", function () { + it("rejects invalid format", async () => { + const encoder = await createTestEncoder(); + await assert.isRejected( + encoder.decodeMessageData({ + body: Buffer.alloc(1), + contentType: "application/json+1234", + }), + /application\/json.*avro\/binary/ + ); + }); + + it("rejects schema with no name", async () => { + const encoder = await createTestEncoder(); + const schema = JSON.stringify({ type: "record", fields: [] }); + await assert.isRejected(encoder.encodeMessageData({}, schema), /name/); + }); + + it("rejects a schema with different format", async () => { + const registry = createTestRegistry(true); // true means never live, we can't register non-avro schema in live service + const encoder = await createTestEncoder(false, registry); + const schema = await registry.registerSchema({ + name: "_", + definition: "_", + format: "NotAvro", + groupName: testGroup, + }); + + await assert.isRejected( + encoder.decodeMessageData({ + body: Buffer.alloc(1), + contentType: `avro/binary+${schema.id}`, + }), + new RegExp(`${schema.id}.*NotAvro.*avro`) + ); + }); + + it("rejects encoding when schema is not found", async () => { + const encoder = await createTestEncoder(false); + const schema = JSON.stringify({ + type: "record", + name: "NeverRegistered", + namespace: "my.example", + fields: [{ name: "count", type: "int" }], + }); + await assert.isRejected(encoder.encodeMessageData({ count: 42 }, schema), /not found/); + }); + + it("rejects decoding when schema is not found", async () => { + const encoder = await createTestEncoder(false); + const payload = testAvroType.toBuffer(testValue); + await assert.isRejected( + encoder.decodeMessageData({ + body: payload, + contentType: `avro/binary+${testSchemaIds[1]}`, + }), + /not found/ + ); + }); + + it("encodes to the expected format", async () => { + const registry = createTestRegistry(); + const schemaId = await registerTestSchema(registry); + const encoder = await createTestEncoder(false, registry); + const message = await encoder.encodeMessageData(testValue, testSchema); + assert.isUndefined((message.body as Buffer).readBigInt64BE); + const buffer = Buffer.from(message.body); + assert.strictEqual(`avro/binary+${schemaId}`, message.contentType); + assert.deepStrictEqual(testAvroType.fromBuffer(buffer), testValue); + }); + + it("decodes from the expected format", async () => { + const registry = createTestRegistry(); + const schemaId = await registerTestSchema(registry); + const encoder = await createTestEncoder(false, registry); + const payload = testAvroType.toBuffer(testValue); + assert.deepStrictEqual( + await encoder.decodeMessageData({ + body: payload, + contentType: `avro/binary+${schemaId}`, + }), + testValue + ); + }); + + it("encodes and decodes in round trip", async () => { + let encoder = await createTestEncoder(); + let message = await encoder.encodeMessageData(testValue, testSchema); + assert.deepStrictEqual(await encoder.decodeMessageData(message), testValue); + + // again for cache hit coverage on encodeMessageData + message = await encoder.encodeMessageData(testValue, testSchema); + assert.deepStrictEqual(await encoder.decodeMessageData(message), testValue); + + // throw away encoder for cache miss coverage on decodeMessageData + encoder = await createTestEncoder(false); + assert.deepStrictEqual(await encoder.decodeMessageData(message), testValue); + + // throw away encoder again and cover getSchemaProperties instead of registerSchema + encoder = await createTestEncoder(false); + assert.deepStrictEqual(await encoder.encodeMessageData(testValue, testSchema), message); + }); + + it("works with trivial example in README", async () => { + const encoder = await createTestEncoder(); + + // Example Avro schema + const schema = JSON.stringify({ + type: "record", + name: "Rating", + namespace: "my.example", + fields: [{ name: "score", type: "int" }], + }); + + // Example value that matches the Avro schema above + const value = { score: 42 }; + + // encode value to a message + const message = await encoder.encodeMessageData(value, schema); + + // Decode message to value + const decodedValue = await encoder.decodeMessageData(message); + + assert.deepStrictEqual(decodedValue, value); + }); + + it("decodes from a compatible reader schema", async () => { + const encoder = await createTestEncoder(); + const message = await encoder.encodeMessageData(testValue, testSchema); + const decodedValue: any = await encoder.decodeMessageData(message, { + /** + * This schema is missing the favoriteNumber field that exists in the writer schema + * and adds an "age" field with a default value. + */ + schema: JSON.stringify({ + type: "record", + name: "AvroUser", + namespace: "com.azure.schemaregistry.samples", + fields: [ + { + name: "name", + type: "string", + }, + { + name: "age", + type: "int", + default: 30, + }, + ], + }), + }); + assert.isUndefined(decodedValue.favoriteNumber); + assert.equal(decodedValue.name, testValue.name); + assert.equal(decodedValue.age, 30); + }); + + it("fails to decode from an incompatible reader schema", async () => { + const encoder = await createTestEncoder(); + const message = await encoder.encodeMessageData(testValue, testSchema); + assert.isRejected( + encoder.decodeMessageData(message, { + schema: JSON.stringify({ + type: "record", + name: "AvroUser", + namespace: "com.azure.schemaregistry.samples", + fields: [ + { + name: "name", + type: "string", + }, + { + name: "age", + type: "int", + }, + ], + }), + }), + /no matching field for default-less com.azure.schemaregistry.samples.AvroUser.age/ + ); + }); +}); diff --git a/sdk/schemaregistry/schema-registry-avro/test/schemaRegistryAvroSerializer.spec.ts b/sdk/schemaregistry/schema-registry-avro/test/schemaRegistryAvroSerializer.spec.ts deleted file mode 100644 index 2f9082d7d632..000000000000 --- a/sdk/schemaregistry/schema-registry-avro/test/schemaRegistryAvroSerializer.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { assert, use as chaiUse } from "chai"; -import chaiPromises from "chai-as-promised"; -import { testAvroType, testGroup, testSchema, testSchemaIds, testValue } from "./utils/dummies"; -import { createTestRegistry } from "./utils/mockedRegistryClient"; -import { createTestSerializer, registerTestSchema } from "./utils/mockedSerializer"; - -chaiUse(chaiPromises); - -describe("SchemaRegistryAvroSerializer", function () { - it("rejects buffers that are too small", async () => { - const serializer = await createTestSerializer(); - await assert.isRejected(serializer.deserialize(Buffer.alloc(3)), /small/); - }); - - it("rejects invalid format", async () => { - const serializer = await createTestSerializer(); - const buffer = Buffer.alloc(42); - buffer.writeUInt32BE(0x1234, 0); - await assert.isRejected(serializer.deserialize(buffer), /format.*0x1234/); - }); - - it("rejects schema with no name", async () => { - const serializer = await createTestSerializer(); - const schema = JSON.stringify({ type: "record", fields: [] }); - await assert.isRejected(serializer.serialize({}, schema), /name/); - }); - - it("rejects a schema with different format", async () => { - const registry = createTestRegistry(true); // true means never live, we can't register non-avro schema in live service - const serializer = await createTestSerializer(false, registry); - const schema = await registry.registerSchema({ - name: "_", - definition: "_", - format: "NotAvro", - groupName: testGroup, - }); - - const buffer = Buffer.alloc(36); - buffer.write(schema.id, 4, 32, "utf-8"); - await assert.isRejected( - serializer.deserialize(buffer), - new RegExp(`${schema.id}.*NotAvro.*avro`) - ); - }); - - it("rejects serialization when schema is not found", async () => { - const serializer = await createTestSerializer(false); - const schema = JSON.stringify({ - type: "record", - name: "NeverRegistered", - namespace: "my.example", - fields: [{ name: "count", type: "int" }], - }); - await assert.isRejected(serializer.serialize({ count: 42 }, schema), /not found/); - }); - - it("rejects deserialization when schema is not found", async () => { - const serializer = await createTestSerializer(false); - const payload = testAvroType.toBuffer(testValue); - const buffer = Buffer.alloc(36 + payload.length); - buffer.write(testSchemaIds[1], 4, 32, "utf-8"); - payload.copy(buffer, 36); - await assert.isRejected(serializer.deserialize(buffer), /not found/); - }); - - it("serializes to the expected format", async () => { - const registry = createTestRegistry(); - const schemaId = await registerTestSchema(registry); - const serializer = await createTestSerializer(false, registry); - const arr = await serializer.serialize(testValue, testSchema); - assert.isUndefined((arr as Buffer).readBigInt64BE); - const buffer = Buffer.from(arr); - assert.strictEqual(0x0, buffer.readUInt32BE(0)); - assert.strictEqual(schemaId, buffer.toString("utf-8", 4, 36)); - const payload = buffer.slice(36); - assert.deepStrictEqual(testAvroType.fromBuffer(payload), testValue); - }); - - it("deserializes from the expected format", async () => { - const registry = createTestRegistry(); - const schemaId = await registerTestSchema(registry); - const serializer = await createTestSerializer(false, registry); - const payload = testAvroType.toBuffer(testValue); - const buffer = Buffer.alloc(36 + payload.length); - - buffer.write(schemaId, 4, 32, "utf-8"); - payload.copy(buffer, 36); - assert.deepStrictEqual(await serializer.deserialize(buffer), testValue); - }); - - it("serializes and deserializes in round trip", async () => { - let serializer = await createTestSerializer(); - let buffer = await serializer.serialize(testValue, testSchema); - assert.deepStrictEqual(await serializer.deserialize(buffer), testValue); - - // again for cache hit coverage on serialize - buffer = await serializer.serialize(testValue, testSchema); - assert.deepStrictEqual(await serializer.deserialize(buffer), testValue); - - // throw away serializer for cache miss coverage on deserialize - serializer = await createTestSerializer(false); - assert.deepStrictEqual(await serializer.deserialize(buffer), testValue); - - // throw away serializer again and cover getSchemaProperties instead of registerSchema - serializer = await createTestSerializer(false); - assert.deepStrictEqual(await serializer.serialize(testValue, testSchema), buffer); - }); - - it("works with trivial example in README", async () => { - const serializer = await createTestSerializer(); - - // Example Avro schema - const schema = JSON.stringify({ - type: "record", - name: "Rating", - namespace: "my.example", - fields: [{ name: "score", type: "int" }], - }); - - // Example value that matches the Avro schema above - const value = { score: 42 }; - - // Serialize value to buffer - const buffer = await serializer.serialize(value, schema); - - // Deserialize buffer to value - const deserializedValue = await serializer.deserialize(buffer); - - assert.deepStrictEqual(deserializedValue, value); - }); -}); diff --git a/sdk/schemaregistry/schema-registry-avro/test/utils/mockedSerializer.ts b/sdk/schemaregistry/schema-registry-avro/test/utils/mockedEncoder.ts similarity index 72% rename from sdk/schemaregistry/schema-registry-avro/test/utils/mockedSerializer.ts rename to sdk/schemaregistry/schema-registry-avro/test/utils/mockedEncoder.ts index 709804cd1527..68f4a9d29944 100644 --- a/sdk/schemaregistry/schema-registry-avro/test/utils/mockedSerializer.ts +++ b/sdk/schemaregistry/schema-registry-avro/test/utils/mockedEncoder.ts @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { SchemaRegistry } from "@azure/schema-registry"; -import { SchemaRegistryAvroSerializer } from "../../src/schemaRegistryAvroSerializer"; import { testGroup, testSchema, testSchemaObject } from "./dummies"; +import { SchemaRegistry } from "@azure/schema-registry"; +import { SchemaRegistryAvroEncoder } from "../../src/schemaRegistryAvroEncoder"; import { createTestRegistry } from "./mockedRegistryClient"; -export async function createTestSerializer( +export async function createTestEncoder( autoRegisterSchemas = true, registry = createTestRegistry() -): Promise { +): Promise { if (!autoRegisterSchemas) { await registerTestSchema(registry); } - return new SchemaRegistryAvroSerializer(registry, { autoRegisterSchemas, groupName: testGroup }); + return new SchemaRegistryAvroEncoder(registry, { autoRegisterSchemas, groupName: testGroup }); } export async function registerTestSchema(registry: SchemaRegistry): Promise { diff --git a/sdk/schemaregistry/schema-registry-avro/test/utils/mockedRegistryClient.ts b/sdk/schemaregistry/schema-registry-avro/test/utils/mockedRegistryClient.ts index 14002caf9b65..d57c0cb78119 100644 --- a/sdk/schemaregistry/schema-registry-avro/test/utils/mockedRegistryClient.ts +++ b/sdk/schemaregistry/schema-registry-avro/test/utils/mockedRegistryClient.ts @@ -11,8 +11,8 @@ import { SchemaRegistry, SchemaRegistryClient, } from "@azure/schema-registry"; -import { ClientSecretCredential } from "@azure/identity"; import { env, isLiveMode } from "@azure-tools/test-recorder"; +import { ClientSecretCredential } from "@azure/identity"; import { testSchemaIds } from "./dummies"; export function createTestRegistry(neverLive = false): SchemaRegistry { diff --git a/sdk/search/search-documents/test/public/utils/recordedClient.ts b/sdk/search/search-documents/test/public/utils/recordedClient.ts index 3eb4ba6ab820..3d6253396cc9 100644 --- a/sdk/search/search-documents/test/public/utils/recordedClient.ts +++ b/sdk/search/search-documents/test/public/utils/recordedClient.ts @@ -61,20 +61,7 @@ export function createClients( indexName: string, serviceVersion: string ): Clients { - let endPoint: string = "https://endpoint"; - - switch (testEnv.AZURE_AUTHORITY_HOST) { - case "https://login.microsoftonline.us": - endPoint = process.env.USENDPOINT ?? "https://endpoint"; - break; - case "https://login.chinacloudapi.cn": - endPoint = process.env.CHINAENDPOINT ?? "https://endpoint"; - break; - default: - endPoint = process.env.ENDPOINT ?? "https://endpoint"; - break; - } - + const endPoint: string = process.env.ENDPOINT ?? "https://endpoint"; const credential = new AzureKeyCredential(testEnv.SEARCH_API_ADMIN_KEY); const searchClient = new SearchClient(endPoint, indexName, credential, { serviceVersion, diff --git a/sdk/search/test-resources.json b/sdk/search/test-resources.json index f3e0c6152616..df178404aff4 100644 --- a/sdk/search/test-resources.json +++ b/sdk/search/test-resources.json @@ -15,6 +15,13 @@ "metadata": { "description": "SKU for search resource. The default is 'basic'" } + }, + "searchEndpointSuffix": { + "type": "string", + "defaultValue": "search.windows.net", + "metadata": { + "description": "Endpoint suffix for the search resource. Defaults to 'search.windows.net'" + } } }, "variables": {}, @@ -47,15 +54,7 @@ }, "ENDPOINT": { "type": "string", - "value": "[concat('https://', parameters('baseName'), '.search.windows.net')]" - }, - "CHINAENDPOINT": { - "type": "string", - "value": "[concat('https://', parameters('baseName'), '.search.azure.cn')]" - }, - "USENDPOINT": { - "type": "string", - "value": "[concat('https://', parameters('baseName'), '.search.azure.us')]" + "value": "[concat('https://', parameters('baseName'), '.', parameters('searchEndpointSuffix'))]" } } }