`core http` dependency migration to `core client` `core rest pipeline`
The @azure/search-documents
has been migrated from @azure/core-http
dependency to @azure/core-client
/@azure/core-rest-pipeline
packages. The PR for the same is available at #17872. The steps and details for the migration have been documented here.
Note: These steps are specific to one migration and might not consist of all possible scenarios.
In the swagger configuration file, you need to use the correct extension. For example, the code:
use-extension:
"@autorest/typescript": "6.0.0-beta.4"
This should be modified as:
use-extension:
"@autorest/typescript": "6.0.0-beta.13"
You can find the latest version to be used at npmjs. The use-core-v2
property must be changed from false
to true
.
Remove the dependency on @azure/core-http
and make a dependency on @azure/core-client
& @azure/core-rest-pipeline
packages.
-
createPipelineFromOptions
- Need not be used after the migration -
InternalPipelineOptions
- Change it toInternalClientPipelineOptions
-
allowedHeaderNames
- Change it toadditionalAllowedHeaderNames
-
RequestPolicyFactory
- Need not be used after the migration -
PipelineOptions
- Change it toCommonClientOptions
-
bearerTokenAuthenticationPolicy
- Change it to usebearerTokenAuthenticationPolicy
fromcore-rest-pipeline
-
RestError
- Change it to useRestError
fromcore-rest-pipeline
-
OperationOptions
- Change it to useOperationOptions
fromcore-client
-
delay
- Import it fromcore-util
-
isNode
- Import it fromcore-util
-
WebResourceLike
- Change it toPipelineRequest
fromcore-rest-pipeline
-
HttpOperationResponse
- Change it toPipelineResponse
fromcore-rest-pipeline
-
RequestOptionsBase
- Change it toOperationOptions
-
DefaultHttpClient
- Change it tocreateDefaultHttpClient
fromcore-rest-pipeline
Note: core-util is still in beta, use a local copy instead if there is a conflict with the package
The following imports should be removed while creating the policies.
- RequestPolicy - Need not be used after the migration
- BaseRequestPolicy - Need not be used after the migration
- HttpOperationResponse - Need not be used after the migration
- RequestPolicyOptionsLike - Need not be used after the migration
Instead, the following imports (from core-rest-pipeline
) should be used: PipelinePolicy
,PipelineRequest
,SendRequest
& PipelineResponse
.
In the constructor of the custom client, if there is any code (similar to the one below) to create and pass userAgentOptions
to GeneratedClient, it should be removed. The userAgentOptions is now handled by GeneratedClient automatically.
const libInfo = `azsdk-js-<service name>/${SDK_VERSION}`;
if (!options.userAgentOptions) {
options.userAgentOptions = {};
}
if (options.userAgentOptions.userAgentPrefix) {
options.userAgentOptions.userAgentPrefix = `${options.userAgentOptions.userAgentPrefix} ${libInfo}`;
} else {
options.userAgentOptions.userAgentPrefix = libInfo;
}
Please DO verify that proper telemetry information is set in the HTTP headers when the SDK is sending out requests.
While using the core-http
, we create a pipeline using createPipelineFromOptions
. This pipeline is passed to the constructor of the GenerateClient. The code looks like:
const pipeline = createPipelineFromOptions(internalPipelineOptions, requestPolicyFactory);
this.client = new GeneratedClient(this.endpoint, apiVersion, pipeline);
As mentioned in the Import Changes section, createPipelineFromOptions
import has been removed. Now, we could pass the internalClientPipelineOptions
directly to the constructor of the genereate client. The code looks like:
this.client = new GeneratedClient(this.endpoint, apiVersion, internalClientPipelineOptions);
While using the core-http
, we create a RequestPolicyFactory
object (before creating an instance of the GeneratedClient) and add or remove policies to it. The code looks like:
const requestPolicyFactory: RequestPolicyFactory = isTokenCredential(credential)
? bearerTokenAuthenticationPolicy(credential, utils.DEFAULT_SEARCH_SCOPE)
: createSearchApiKeyCredentialPolicy(credential);
const pipeline = createPipelineFromOptions(internalPipelineOptions, requestPolicyFactory);
if (Array.isArray(pipeline.requestPolicyFactories)) {
pipeline.requestPolicyFactories.unshift(odataMetadataPolicy("minimal"));
}
With the migration, the RequestPolicyFactory
has been removed. The policies should be added/removed AFTER the creating an instance of the GeneratedClient. The code looks like:
this.client = new GeneratedClient(this.endpoint, apiVersion, internalClientPipelineOptions);
if (isTokenCredential(credential)) {
this.client.pipeline.addPolicy(
bearerTokenAuthenticationPolicy({ credential, scopes: utils.DEFAULT_SEARCH_SCOPE })
);
} else {
this.client.pipeline.addPolicy(createSearchApiKeyCredentialPolicy(credential));
}
this.client.pipeline.addPolicy(createOdataMetadataPolicy("minimal"));
While using the core-http
, we call the method operationOptionsToRequestOptionsBase
with the user passed options, before calling the generated methods. This is done to ensure the options are modified to specific structure (of RequestOptions
). The code looks like:
public async listSynonymMaps(options: ListSynonymMapsOptions = {}): Promise<Array<SynonymMap>> {
const { span, updatedOptions } = createSpan("SearchIndexClient-listSynonymMaps", options);
try {
const result = await this.client.synonymMaps.list(operationOptionsToRequestOptionsBase(updatedOptions));
This requirement of changing the options to specific structure has been removed with the migration. Now, it can be passed directly. The code looks like:
public async listSynonymMaps(options: ListSynonymMapsOptions = {}): Promise<Array<SynonymMap>> {
const { span, updatedOptions } = createSpan("SearchIndexClient-listSynonymMaps", options);
try {
const result = await this.client.synonymMaps.list(updatedOptions);
While using the core-http
, there are times where we read _response
property (in both src
and test
code). The code looks like:
public async indexDocuments(batch: IndexDocumentsBatch<T>, options: IndexDocumentsOptions = {}): Promise<IndexDocumentsResult> {
const { span, updatedOptions } = createSpan("SearchClient-indexDocuments", options);
try {
const result = await this.client.documents.index(
{ actions: serialize(batch.actions) },
operationOptionsToRequestOptionsBase(updatedOptions)
);
if (options.throwOnAnyFailure && result._response.status === 207) {
With the migration, the _response
has been removed. We should access the value using the onResponse
method. The code looks like:
public async indexDocuments(batch: IndexDocumentsBatch<T>, options: IndexDocumentsOptions = {}): Promise<IndexDocumentsResult> {
const { span, updatedOptions } = createSpan("SearchClient-indexDocuments", options);
try {
let status: number = 0;
const result = await this.client.documents.index(
{ actions: serialize(batch.actions) },
{
...updatedOptions,
onResponse: (response) => {
status = response.status;
}
}
);
if (options.throwOnAnyFailure && status === 207) {
Sometimes, there are tests that use mock clients with error responses just for the sake of the test. Usually, those clients would override a method by returning a _response
, take this example from KeyVault:
it("403 doesn't throw", async function () {
const code = 403;
const client: any = {
async recoverDeletedCertificate(): Promise<any> {
return {
_response: {
bodyAsText: JSON.stringify({
id: "/version/name/version",
recoveryId: "something",
}),
},
};
},
async getCertificate(): Promise<any> {
throw new RestError(`${code}`, { statusCode: code });
},
};
[...]
});
Since the _response
property is no longer used in CoreV2, instead of returning an object the fake client should create a response and call .onResponse
. So you'll end up with something like this:
it("403 doesn't throw", async function () {
const code = 403;
const fooClient: Partial<KeyVaultClient> = {
async recoverDeletedCertificate(_a,_b,c): Promise<any> {
const request: PipelineRequest = {url: "", method: "GET", headers: createHttpHeaders(), timeout: 100, withCredentials: false, requestId: "something"};
const body = {
id: "/version/name/version",
recoveryId: "something",
}
const response : FullOperationResponse = {
request: request,
bodyAsText: JSON.stringify(body),
status: code,
headers: createHttpHeaders(),
parsedBody: body,
};
c?.onResponse && c.onResponse(response, response , {});
},
async getCertificate(): Promise<any> {
throw new RestError(`${code}`, { statusCode: code });
},
};
[...]
});
While using the core-http
, there are places where you could use Fields extend keyof T
. The code looks like:
public async getDocument<Fields extends keyof T>(key: string, options: GetDocumentOptions<Fields> = {}): Promise<T> {}
With the migration, it has to be modified with Extract
property. The code should be modified as:
public async getDocument<Fields extends Extract<keyof T, string>>(key: string, options: GetDocumentOptions<Fields> = {}): Promise<T> {
While using the core-http
, we use custom class that extends BaseRequestPolicy
with a sendRequest
method (with webResource
parameter). The code looks like:
export function createSearchApiKeyCredentialPolicy(
credential: KeyCredential
): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new SearchApiKeyCredentialPolicy(nextPolicy, options, credential);
}
};
}
/**
* A concrete implementation of an AzureKeyCredential policy
* using the appropriate header for Azure Cognitive Search
*/
class SearchApiKeyCredentialPolicy extends BaseRequestPolicy {
private credential: KeyCredential;
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
credential: KeyCredential
) {
super(nextPolicy, options);
this.credential = credential;
}
public async sendRequest(webResource: WebResourceLike): Promise<HttpOperationResponse> {
if (!webResource) {
throw new Error("webResource cannot be null or undefined");
}
webResource.headers.set(API_KEY_HEADER_NAME, this.credential.key);
return this._nextPolicy.sendRequest(webResource);
}
}
This has been simplified in the core-rest-pipeline
model. You could create the custom policy like this:
const searchApiKeyCredentialPolicy = "SearchApiKeyCredentialPolicy";
export function createSearchApiKeyCredentialPolicy(credential: KeyCredential): PipelinePolicy {
return {
name: searchApiKeyCredentialPolicy,
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
if (!request.headers.has(API_KEY_HEADER_NAME)) {
request.headers.set(API_KEY_HEADER_NAME, credential.key);
}
return next(request);
}
};
}
It is possible that unit tests will start failing after doing the migration, which could be caused by the test recordings not being up to date. Consider re-recording the tests after doing the generation and migration. More information on the new test recorder can be found in this document.
Old core package used to depend on XhrHttpRequest
, which is no longer needed in Core V2. However, old recorder still uses it, so it's a good idea to migrate the recorder to the new one when you are doing this migration. If you are facing a fetch error when running your tests, you have to options:
- Migrate to the new recorder, here's a detailed guide for this. -or-
- Update your test client to include an XhrHttpClient in the options:
Import
createXhrHttpClient
fromtest-utils
. Then, in the test client definition add anhttpClient
option that includescreateXhrHttpClient
only for browser tests. Here's an example:
const client = new SecretClient(keyVaultUrl, credential, {
serviceVersion,
httpClient: isNode ? undefined : createXhrHttpClient(),
});
If you encounter an error like:
Secret /deletetSecrets was not found...
Take a look at the request's URL. If the URL has the path duplicated, like https://mykeyvault.vault.azure.net/deletedsecrets/deletedsecrets?api-version...
this is caused by the iterator being in in the convienence layer, so you will need to make changes in there.
Any method call that passes a continuationToken should be replaced by a method of the same name with the sufix Next
. For example: getDeletedSecret(continuationToken, options )
should be changed to getDeletedSecretNext(vaultURI, continuationToken, options)
.
Another solution is to generate the client with the flag disable-async-iterators
set to false
, this will generate the async iterators and there will be no need for them in the convenience layer.
Metadata in Package.json
The sdk bot uses metadata in the package.json to automatically upgrade the package version in the code after each release, like in this PR. Here is an example of how the metadata section looks in the Mixed Reality package.json:
"//metadata": {
"constantPaths": [
{
"path": "src/generated/mixedRealityStsRestClientContext.ts",
"prefix": "packageVersion"
},
{
"path": "src/constants.ts",
"prefix": "SDK_VERSION"
},
{
"path": "swagger/README.md",
"prefix": "package-version"
}
]
},
When doing the minor version bump, also check if all paths are covering all the constants where the package version is being specified and add any missing constant path to the array.
Local copy of `isNode`
Create a new file isNode.ts
and import isNode
from it, in that file add the following code:
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
* A constant that indicates whether the environment the code is running is Node.JS.
*/
export const isNode =
typeof process !== "undefined" && Boolean(process.version) && Boolean(process.versions?.node);
In that case, another file named isNode.browser.ts
is needed. Copy the following into that file:
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
* A constant that indicates whether the environment the code is running is Node.JS.
*/
export const isNode = false;
Next, add a browser mapping in the package.json, similar to this example from Core:
browser": {
"./dist-esm/src/isNode.js": "./dist-esm/src/isNode.browser.js"
},