diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 31ffdb1860f2..47e34d984ca1 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -21,6 +21,7 @@ dependencies: '@rush-temp/core-auth': file:projects/core-auth.tgz '@rush-temp/core-client': file:projects/core-client.tgz '@rush-temp/core-client-1': file:projects/core-client-1.tgz + '@rush-temp/core-client-paging': file:projects/core-client-paging.tgz '@rush-temp/core-crypto': file:projects/core-crypto.tgz '@rush-temp/core-http': file:projects/core-http.tgz '@rush-temp/core-lro': file:projects/core-lro.tgz @@ -8154,7 +8155,7 @@ packages: dev: false name: '@rush-temp/agrifood-farming' resolution: - integrity: sha512-F6n2fF7nYjo1puQJoehl5wZ2ETNbVOh5R24BoZf1DpjBylMBLYK9XUk/V4ffDHUFaSl1G+CRuO5d6zPMmqLI2Q== + integrity: sha512-1TBfH8mrt2ib3U1qef5slQiSujNaeVSuPS4ISdNcq6Vu2vUIjiJ1oxuXA6rfaaeSG1mGHp9wJDJ7Li0h5wsF1Q== tarball: file:projects/agrifood-farming.tgz version: 0.0.0 file:projects/ai-anomaly-detector.tgz: @@ -9046,6 +9047,44 @@ packages: integrity: sha512-SG/UNSQX+LFioBKFma6ZxRnA/Z5bzBG+UUyTKgfFLuFFMLHGeKSYfH8yrepW+iivLnbClPQfcVes07ZljoR6vQ== tarball: file:projects/core-client-1.tgz version: 0.0.0 + file:projects/core-client-paging.tgz: + dependencies: + '@azure/core-rest-pipeline': 1.0.4 + '@microsoft/api-extractor': 7.13.2 + '@types/chai': 4.2.19 + '@types/mocha': 7.0.2 + '@types/node': 8.10.66 + chai: 4.3.4 + cross-env: 7.0.3 + eslint: 7.29.0 + inherits: 2.0.4 + karma: 6.3.4 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.0.3 + karma-edge-launcher: 0.4.2_karma@6.3.4 + karma-env-preprocessor: 0.1.1 + karma-firefox-launcher: 1.3.0 + karma-ie-launcher: 1.0.0_karma@6.3.4 + karma-junit-reporter: 2.0.1_karma@6.3.4 + karma-mocha: 2.0.1 + karma-mocha-reporter: 2.2.5_karma@6.3.4 + karma-sourcemap-loader: 0.3.8 + mocha: 7.2.0 + mocha-junit-reporter: 1.23.3_mocha@7.2.0 + prettier: 2.2.1 + rimraf: 3.0.2 + rollup: 1.32.1 + sinon: 9.2.4 + tslib: 2.3.0 + typedoc: 0.15.2 + typescript: 4.2.4 + util: 0.12.4 + dev: false + name: '@rush-temp/core-client-paging' + resolution: + integrity: sha512-MWsd9fmRdJ/ArkZHl867uBk7W5lA+voMiDFivxzcJSuFVG85nOEPNnoZz8AFlqbnLrhV2r3EOwbsp2M+s3iFHg== + tarball: file:projects/core-client-paging.tgz + version: 0.0.0 file:projects/core-client.tgz: dependencies: '@azure/core-rest-pipeline': 1.0.4 @@ -9081,7 +9120,7 @@ packages: dev: false name: '@rush-temp/core-client' resolution: - integrity: sha512-KnWCuWw5xZmHZQX21uqtlzRlZe1LZQVtuavo4FBOjiivGamirzND/+QxMOfW7m4DlV/htMeT1bg1CNSxOTVKmA== + integrity: sha512-7b3K4L1f+at6Zz1whbFBdVfAgZlrvpPudcOgwbBeXPyJumHKk4pi5t4LVsKGfTOWxzFv31rPICyaJZ5IncvLhg== tarball: file:projects/core-client.tgz version: 0.0.0 file:projects/core-crypto.tgz: @@ -11919,6 +11958,7 @@ specifiers: '@rush-temp/core-auth': file:./projects/core-auth.tgz '@rush-temp/core-client': file:./projects/core-client.tgz '@rush-temp/core-client-1': file:./projects/core-client-1.tgz + '@rush-temp/core-client-paging': file:./projects/core-client-paging.tgz '@rush-temp/core-crypto': file:./projects/core-crypto.tgz '@rush-temp/core-http': file:./projects/core-http.tgz '@rush-temp/core-lro': file:./projects/core-lro.tgz diff --git a/rush.json b/rush.json index eb5e506f2aeb..ddf01f788db5 100644 --- a/rush.json +++ b/rush.json @@ -436,6 +436,11 @@ "projectFolder": "sdk/core/core-client-rest", "versionPolicyName": "core" }, + { + "packageName": "@azure-rest/core-client-paging", + "projectFolder": "sdk/core/core-client-paging-rest", + "versionPolicyName": "core" + }, { "packageName": "@azure/core-asynciterator-polyfill", "projectFolder": "sdk/core/core-asynciterator-polyfill", diff --git a/sdk/agrifood/agrifood-farming-rest/CHANGELOG.md b/sdk/agrifood/agrifood-farming-rest/CHANGELOG.md index a4eb6307d029..e9261131b5e5 100644 --- a/sdk/agrifood/agrifood-farming-rest/CHANGELOG.md +++ b/sdk/agrifood/agrifood-farming-rest/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.0.0-beta.2 (Unreleased) +### Features Added + +- Export pagination helper function. [#15831](https://github.com/Azure/azure-sdk-for-js/pull/15831) ## 1.0.0-beta.1 (2021-05-26) diff --git a/sdk/agrifood/agrifood-farming-rest/package.json b/sdk/agrifood/agrifood-farming-rest/package.json index 0c27893927cf..f431a0a52a71 100644 --- a/sdk/agrifood/agrifood-farming-rest/package.json +++ b/sdk/agrifood/agrifood-farming-rest/package.json @@ -85,7 +85,8 @@ "autoPublish": false, "dependencies": { "@azure/core-auth": "^1.3.0", - "@azure-rest/core-client": "1.0.0-beta.4", + "@azure-rest/core-client-paging": "1.0.0-beta.1", + "@azure-rest/core-client": "1.0.0-beta.5", "@azure/core-rest-pipeline": "^1.1.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" diff --git a/sdk/agrifood/agrifood-farming-rest/review/agrifood-farming.api.md b/sdk/agrifood/agrifood-farming-rest/review/agrifood-farming.api.md index 3484b5c12ec7..f1b91823a351 100644 --- a/sdk/agrifood/agrifood-farming-rest/review/agrifood-farming.api.md +++ b/sdk/agrifood/agrifood-farming-rest/review/agrifood-farming.api.md @@ -7,6 +7,8 @@ import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { HttpResponse } from '@azure-rest/core-client'; +import { PagedAsyncIterableIterator } from '@azure-rest/core-client-paging'; +import { PathUncheckedResponse } from '@azure-rest/core-client'; import { RequestParameters } from '@azure-rest/core-client'; import { TokenCredential } from '@azure/core-auth'; @@ -1921,6 +1923,9 @@ export type GeoJsonObject = Polygon | MultiPolygon | Point; // @public (undocumented) export type GeoJsonObjectType = "Point" | "Polygon" | "MultiPolygon"; +// @public +export type GetArrayType = T extends Array ? TData : never; + // @public (undocumented) export interface HarvestData { area?: Measure; @@ -2589,6 +2594,16 @@ export interface OAuthTokensListQueryParamProperties { minLastModifiedDateTime?: Date; } +// @public +export function paginate(client: Client, initialResponse: TReturn): PagedAsyncIterableIterator, PaginateReturn[]>; + +// @public +export type PaginateReturn = TResult extends { + body: { + value?: infer TPage; + }; +} ? GetArrayType : Array; + // @public (undocumented) export interface Paths1LxjoxzFarmersFarmeridAttachmentsAttachmentidPatchRequestbodyContentMultipartFormDataSchema { createdDateTime?: string; diff --git a/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts b/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts index bcab0f874357..557886f7a5f8 100644 --- a/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts +++ b/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts @@ -8,7 +8,7 @@ * @azsdk-weight 20 */ -import FarmBeats, { Farmer } from "@azure-rest/agrifood-farming"; +import FarmBeats, { paginate } from "@azure-rest/agrifood-farming"; import { DefaultAzureCredential } from "@azure/identity"; import dotenv from "dotenv"; @@ -18,34 +18,18 @@ const endpoint = process.env["FARMBEATS_ENDPOINT"] || ""; async function main() { const farming = FarmBeats(endpoint, new DefaultAzureCredential()); + const response = await farming.path("/farmers").get(); - const result = await farming.path("/farmers").get(); - - if (result.status !== "200") { - throw result.body.error?.message; + if (response.status !== "200") { + throw response.body.error || new Error(`Unexpected status code ${response.status}`); } - let farmers: Farmer[] = result.body.value ?? []; - let skipToken = result.body.skipToken; - - // Farmer results may be paginated. In case there are more than one page of farmers - // the service would return a skipToken that can be used for subsequent request to get - // the next page of farmers. Here we'll keep calling until the service stops returning a - // skip token which means that there are no more pages. - while (skipToken) { - const page = await farming.path("/farmers").get({ queryParameters: { $skipToken: skipToken } }); - if (page.status !== "200") { - throw page.body.error; - } - - farmers.concat(page.body.value ?? []); - skipToken = page.body.skipToken; - } + const farmers = paginate(farming, response); // Lof each farmer id - farmers.forEach((farmer) => { + for await (const farmer of farmers) { console.log(farmer.id); - }); + } } main().catch(console.error); diff --git a/sdk/agrifood/agrifood-farming-rest/src/index.ts b/sdk/agrifood/agrifood-farming-rest/src/index.ts index 9a420561b89e..8bc909814a2a 100644 --- a/sdk/agrifood/agrifood-farming-rest/src/index.ts +++ b/sdk/agrifood/agrifood-farming-rest/src/index.ts @@ -8,4 +8,6 @@ export * from "./models"; export * from "./parameters"; export * from "./responses"; +export { paginate, PaginateReturn, GetArrayType } from "./paging"; + export default FarmBeats; diff --git a/sdk/agrifood/agrifood-farming-rest/src/paging.ts b/sdk/agrifood/agrifood-farming-rest/src/paging.ts new file mode 100644 index 000000000000..2dabe2eeb0d0 --- /dev/null +++ b/sdk/agrifood/agrifood-farming-rest/src/paging.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { paginateResponse, PagedAsyncIterableIterator } from "@azure-rest/core-client-paging"; +import { Client, PathUncheckedResponse } from "@azure-rest/core-client"; + +/** + * Helper type to extract the type of an array + */ +export type GetArrayType = T extends Array ? TData : never; + +/** + * Helper type to infer the Type of the paged elements from the response type + * This type is generated based on the swagger information for x-ms-pageable + * specifically on the itemName property which indicates the property of the response + * where the page items are found. The default value is `value`. + * This type will allow us to provide strongly typed Iterator based on the response we get as second parameter + */ +export type PaginateReturn = TResult extends { + body: { value?: infer TPage }; +} + ? GetArrayType + : Array; + +/** + * This is the wrapper function that would be exposed. It is hiding the Pagination Options because it can be + * obtained from the swagger + * @param client - Client to use for sending the next page requests + * @param initialResponse - Initial response containing the nextLink and current page of elements + * @returns - PagedAsyncIterableIterator to iterate the elements + */ +export function paginate( + client: Client, + initialResponse: TReturn +): PagedAsyncIterableIterator, PaginateReturn[]> { + return paginateResponse>(client, initialResponse); +} diff --git a/sdk/agrifood/agrifood-farming-rest/test/public/smoke.spec.ts b/sdk/agrifood/agrifood-farming-rest/test/public/smoke.spec.ts index a8388c21c081..ffe9c1cd6ebe 100644 --- a/sdk/agrifood/agrifood-farming-rest/test/public/smoke.spec.ts +++ b/sdk/agrifood/agrifood-farming-rest/test/public/smoke.spec.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { FarmBeatsRestClient } from "../../src"; +import { FarmBeatsRestClient, Farmer, paginate } from "../../src"; import { Recorder } from "@azure/test-utils-recorder"; import { assert } from "chai"; @@ -29,7 +29,14 @@ describe("List farmers", () => { assert.fail(`GET "/farmers" failed with ${result.status}`); } - assert.isDefined(result.body.value?.length); + const farmers = paginate(client, result); + + let lastFarmer: Farmer | undefined = undefined; + for await (const farmer of farmers) { + lastFarmer = farmer; + } + + assert.isDefined(lastFarmer); }); it("should create a farmer", async () => { diff --git a/sdk/confidentialledger/confidential-ledger-rest/package.json b/sdk/confidentialledger/confidential-ledger-rest/package.json index be96d264d97f..9c1f361f9106 100644 --- a/sdk/confidentialledger/confidential-ledger-rest/package.json +++ b/sdk/confidentialledger/confidential-ledger-rest/package.json @@ -85,7 +85,7 @@ "autoPublish": false, "dependencies": { "@azure/core-auth": "^1.3.0", - "@azure-rest/core-client": "1.0.0-beta.4", + "@azure-rest/core-client": "1.0.0-beta.5", "@azure/core-rest-pipeline": "^1.1.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" diff --git a/sdk/core/ci.yml b/sdk/core/ci.yml index 0f0b8f5e85fc..f3fb45ea2feb 100644 --- a/sdk/core/ci.yml +++ b/sdk/core/ci.yml @@ -48,6 +48,8 @@ extends: safeName: azurecoreclient - name: azure-rest-core-client safeName: azurerestcoreclient + - name: azure-rest-core-client-paging + safeName: azurerestcoreclientpaging - name: azure-core-crypto safeName: azurecorecrypto - name: azure-core-http diff --git a/sdk/core/core-client-paging-rest/CHANGELOG.md b/sdk/core/core-client-paging-rest/CHANGELOG.md new file mode 100644 index 000000000000..2238faf06c29 --- /dev/null +++ b/sdk/core/core-client-paging-rest/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release History + +## 1.0.0-beta.1 (UNRELEASED) + +- First release of package, see README.md for details. diff --git a/sdk/core/core-client-paging-rest/LICENSE b/sdk/core/core-client-paging-rest/LICENSE new file mode 100644 index 000000000000..ea8fb1516028 --- /dev/null +++ b/sdk/core/core-client-paging-rest/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/core/core-client-paging-rest/README.md b/sdk/core/core-client-paging-rest/README.md new file mode 100644 index 000000000000..9d4e9c57b5e7 --- /dev/null +++ b/sdk/core/core-client-paging-rest/README.md @@ -0,0 +1,112 @@ +# Azure Rest Core Paging library for JavaScript (Experimental) + +This library is primarily intended to be used in code generated by [AutoRest](https://github.com/Azure/Autorest) and [`autorest.typescript`](https://github.com/Azure/autorest.typescript). Specifically for rest level clients, as a helper to handle Pageable operations. This package implements support for Autorest `x-ms-pageable` specification. + +## Getting started + +### Requirements + +- [Node.js](https://nodejs.org) LTS + +### Installation + +This package is primarily used in generated code and not meant to be consumed directly by end users. + +## Key concepts + +### Helper function `paginateResponse` + +Paginate response is a helper function to handle pagination for the user. Given a response that contains a body with a link to the next page and an array with the current page of results, this helper returns a PagedAsyncIterableIterator that can be used to get all the items or page by page. + +In order to provide better typings, the library that consumes `paginateResponse` can wrap it providing additional types. For example a code generator may consume and export in the following way + +#### Typescript + +```typescript +/** + * This is the wrapper function that would be exposed. It is hiding the Pagination Options because it can be + * obtained in the case of a generator from the Swagger definition or by a developer context knowledge in case of a + * hand written library. + */ +export function paginate( + client: Client, + initialResponse: TReturn +): PagedAsyncIterableIterator, PaginateReturn[]> { + return paginateResponse>(client, initialResponse, { + // For example these values could come from the swagger + itemName: "items", + nextLinkName: "continuationLink", + }); +} + +// Helper type to extract the type of an array +type GetArrayType = T extends Array ? TData : never; + +// Helper type to infer the Type of the paged elements from the response type +// This type will be generated based on the swagger information for x-ms-pageable +// specifically on the itemName property which indicates the property of the response +// where the page items are found. The default value is `value`. +// This type will allow us to provide strongly typed Iterator based on the response we get as second parameter +export type PaginateReturn = TResult extends { + body: { items: infer TPage }; +} + ? GetArrayType + : Array; + +// Usage +const client = Client("https://example.org", new DefaultAzureCredentials()); + +const response = client.path("/foo").get(); +const items = paginate(client, response); + +for await (const item of items) { + console.log(item.name); +} +``` + +#### JavaScript + +```javascript +/** + * This is the wrapper function that would be exposed. It is hiding the Pagination Options because it can be + * obtained in the case of a generator from the Swagger definition or by a developer context knowledge in case of a + * hand written library. + */ +export function paginate(client, initialResponse) { + return paginateResponse(client, initialResponse, { + // For example these values could come from the swagger + itemName: "items", + nextLinkName: "continuationLink", + }); +} + +// Usage +const client = Client("https://example.org", new DefaultAzureCredentials()); + +const response = client.path("/foo").get(); +const items = paginate(client, response); + +for await (const item of items) { + console.log(item.name); +} +``` + +## Examples + +Examples can be found in the `samples` folder. + +## Next steps + +You can build and run the tests locally by executing `rushx test`. Explore the `test` folder to see advanced usage and behavior of the public classes. + +Learn more about [AutoRest](https://github.com/Azure/autorest) and the [autorest.typescript extension](https://github.com/Azure/autorest.typescript) for generating a compatible client on top of this package. + +## Troubleshooting + +If you run into issues while using this library, please feel free to [file an issue](https://github.com/Azure/azure-sdk-for-js/issues/new). + +## Contributing + +If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/master/CONTRIBUTING.md) to learn more about how to build and test the code. + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-js%2Fsdk%2Fcore-rest%2Fcore-client%2FREADME.png) diff --git a/sdk/core/core-client-paging-rest/api-extractor.json b/sdk/core/core-client-paging-rest/api-extractor.json new file mode 100644 index 000000000000..5f0bb62e9090 --- /dev/null +++ b/sdk/core/core-client-paging-rest/api-extractor.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/latest/src/index.d.ts", + "docModel": { + "enabled": true + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/latest/core-client-paging-rest.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } + } + } +} diff --git a/sdk/core/core-client-paging-rest/karma.conf.js b/sdk/core/core-client-paging-rest/karma.conf.js new file mode 100644 index 000000000000..005c7f1c5a55 --- /dev/null +++ b/sdk/core/core-client-paging-rest/karma.conf.js @@ -0,0 +1,121 @@ +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); + +module.exports = function (config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "./", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-sourcemap-loader", + "karma-junit-reporter", + ], + + // list of files / patterns to load in the browser + files: [ + // Uncomment the cdn link below for the polyfill service to support IE11 missing features + // Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys + // "https://cdn.polyfill.io/v2/polyfill.js?features=Symbol,Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys|always", + "dist-test/index.browser.js", + ], + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + "**/*.js": ["sourcemap", "env"], + // IMPORTANT: COMMENT following line if you want to debug in your browsers!! + // Preprocess source file to calculate code coverage, however this will make source file unreadable + //"dist-test/index.browser.js": ["coverage"] + }, + + // inject following environment values into browser testing with window.__env__ + // environment values MUST be exported or set with same console running "karma start" + // https://www.npmjs.com/package/karma-env-preprocessor + // EXAMPLE: envPreprocessor: ["ACCOUNT_NAME", "ACCOUNT_SAS"], + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["mocha", "coverage", "junit"], + + coverageReporter: { + // specify a common output directory + dir: "coverage-browser/", + reporters: [ + { type: "json", subdir: ".", file: "coverage.json" }, + { type: "lcovonly", subdir: ".", file: "lcov.info" }, + { type: "html", subdir: "html" }, + { type: "cobertura", subdir: ".", file: "cobertura-coverage.xml" }, + ], + }, + + junitReporter: { + outputDir: "", // results will be saved as $outputDir/$browserName.xml + outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: "", // suite will become the package name attribute in xml testsuite element + useBrowserName: false, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {}, // key value pair of properties to add to the section of the report + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + // 'ChromeHeadless', 'Chrome', 'Firefox', 'Edge', 'IE' + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000", + }, + }, + }); +}; diff --git a/sdk/core/core-client-paging-rest/package.json b/sdk/core/core-client-paging-rest/package.json new file mode 100644 index 000000000000..280db0713bc3 --- /dev/null +++ b/sdk/core/core-client-paging-rest/package.json @@ -0,0 +1,101 @@ +{ + "name": "@azure-rest/core-client-paging", + "version": "1.0.0-beta.1", + "description": "A helper library which implements Autorest x-ms-pageable spec for pagination.", + "sdk-type": "core", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", + "types": "types/latest/core-client-paging-rest.d.ts", + "browser": { + "./dist-esm/src/url.js": "./dist-esm/src/url.browser.js" + }, + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "build:browser": "npm run build:ts && cross-env ONLY_BROWSER=true rollup -c 2>&1", + "build:node": "npm run build:ts && cross-env ONLY_NODE=true rollup -c 2>&1", + "build:samples": "echo Skipped.", + "build:test": "tsc -p . && rollup -c 2>&1", + "build:ts": "tsc -p .", + "build": "npm run build:ts && rollup -c 2>&1 && api-extractor run --local", + "check-format": "prettier --list-different \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-* types *.tgz *.log", + "execute:samples": "echo skipped", + "extract-api": "npm run build:ts && api-extractor run --local", + "format": "prettier --write \"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", + "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json api-extractor.json src test --ext .ts", + "pack": "npm pack 2>&1", + "prebuild": "npm run clean", + "test:browser": "npm run clean && npm run build:test && npm run unit-test:browser", + "test:node": "npm run clean && npm run build:test && npm run unit-test:node", + "test": "npm run clean && npm run build:test && npm run unit-test", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "mocha -r esm --require ts-node/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 1200000 --full-trace \"test/{,!(browser)/**/}*.spec.ts\"", + "unit-test": "npm run unit-test:node && npm run unit-test:browser", + "docs": "typedoc --excludePrivate --excludeNotExported --excludeExternals --stripInternal --mode file --out ./dist/docs ./src" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/src/latest/core-client-paging-rest.d.ts", + "README.md", + "LICENSE" + ], + "repository": "github:Azure/azure-sdk-for-js", + "keywords": [ + "azure", + "cloud" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "engines": { + "node": ">=8.0.0" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client-paging-rest/", + "sideEffects": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "dependencies": { + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.0.3", + "@azure-rest/core-client": "1.0.0-beta.5", + "tslib": "^2.2.0" + }, + "devDependencies": { + "@microsoft/api-extractor": "7.13.2", + "@types/chai": "^4.1.6", + "@types/mocha": "^7.0.2", + "@types/node": "^8.0.0", + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "@azure/dev-tool": "^1.0.0", + "chai": "^4.2.0", + "cross-env": "^7.0.2", + "eslint": "^7.15.0", + "inherits": "^2.0.3", + "karma": "^6.2.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "karma-sourcemap-loader": "^0.3.8", + "mocha": "^7.1.1", + "mocha-junit-reporter": "^1.18.0", + "prettier": "2.2.1", + "rimraf": "^3.0.0", + "rollup": "^1.16.3", + "sinon": "^9.0.2", + "typescript": "~4.2.0", + "util": "^0.12.1", + "typedoc": "0.15.2" + } +} diff --git a/sdk/core/core-client-paging-rest/review/core-client-paging.api.md b/sdk/core/core-client-paging-rest/review/core-client-paging.api.md new file mode 100644 index 000000000000..e3a6c7305a9c --- /dev/null +++ b/sdk/core/core-client-paging-rest/review/core-client-paging.api.md @@ -0,0 +1,23 @@ +## API Report File for "@azure-rest/core-client-paging" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Client } from '@azure-rest/core-client'; +import { HttpResponse } from '@azure-rest/core-client'; +import { PagedAsyncIterableIterator } from '@azure/core-paging'; + +export { PagedAsyncIterableIterator } + +// @public +export interface PaginateOptions { + itemName?: string; + nextLinkName?: string | null; +} + +// @public +export function paginateResponse(client: Client, initialResponse: HttpResponse, options?: PaginateOptions): PagedAsyncIterableIterator; + + +``` diff --git a/sdk/core/core-client-paging-rest/rollup.config.js b/sdk/core/core-client-paging-rest/rollup.config.js new file mode 100644 index 000000000000..26e83ddfafa4 --- /dev/null +++ b/sdk/core/core-client-paging-rest/rollup.config.js @@ -0,0 +1,2 @@ +import { makeConfig } from "@azure/dev-tool/shared-config/rollup"; +export default makeConfig(require("./package.json")); diff --git a/sdk/core/core-client-paging-rest/src/index.ts b/sdk/core/core-client-paging-rest/src/index.ts new file mode 100644 index 000000000000..902316ffaffd --- /dev/null +++ b/sdk/core/core-client-paging-rest/src/index.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * A helper library which implements Autorest x-ms-pageable spec for pagination + * + * @packageDocumentation + */ +export { paginateResponse, PaginateOptions } from "./paginate"; +export { PagedAsyncIterableIterator } from "@azure/core-paging"; diff --git a/sdk/core/core-client-paging-rest/src/paginate.ts b/sdk/core/core-client-paging-rest/src/paginate.ts new file mode 100644 index 000000000000..9aa5796ddabe --- /dev/null +++ b/sdk/core/core-client-paging-rest/src/paginate.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// + +import { + Client, + createRestError, + HttpResponse, + PathUncheckedResponse, +} from "@azure-rest/core-client"; +import { PagedAsyncIterableIterator } from "@azure/core-paging"; + +const Http2xxStatusCodes = ["200", "201", "202", "203", "204", "205", "206", "207", "208", "226"]; + +const DEFAULT_NEXTLINK = "nextLink"; +const DEFAULT_VALUES = "value"; + +/** + * Options to indicate custom values for where to look for nextLink and values + * when paginating a response + */ +export interface PaginateOptions { + /** + * Property name in the body where the nextLink is located + * The default value is `nextLink`. + * nextLink is an opaque URL for the client, in which the next set of results is located. + * Note: if nextLinkName is set to `null` only the first page is returned, no additional + * requests are made. + */ + nextLinkName?: string | null; + /** + * Indicates the name of the property in which the set of values is found. Default: `value` + */ + itemName?: string; +} + +/** + * Helper to iterate pageable responses + * @param client - Client to use for sending the request to get additional pages + * @param initialResponse - The initial response + * @param options - Options to use custom property names for pagination + * @returns - return a PagedAsyncIterableIterator that can be used to iterate the elements + */ +export function paginateResponse( + client: Client, + initialResponse: HttpResponse, + options: PaginateOptions = {} +): PagedAsyncIterableIterator { + const iter = listAll(client, initialResponse, options); + return { + next() { + return iter.next(); + }, + [Symbol.asyncIterator]() { + return this; + }, + byPage: () => { + return listPage(client, initialResponse, options); + }, + }; +} + +async function* listAll( + client: Client, + initialResponse: PathUncheckedResponse, + paginateOptions: PaginateOptions +): AsyncIterableIterator { + for await (const page of listPage(client, initialResponse, paginateOptions)) { + yield* page; + } +} + +async function* listPage[]>( + client: Client, + initialResponse: PathUncheckedResponse, + options: PaginateOptions +): AsyncIterableIterator { + let result = initialResponse; + checkPagingRequest(result); + let nextLink = getNextLink(result.body, options); + let values = getElements(result.body, options); + + yield values; + + // According to x-ms-pageable is the nextLinkName is set to null we should only + // return the first page and skip any additional queries even if the initial response + // contains a nextLink. + if (options.nextLinkName === null) { + return; + } + + while (nextLink) { + result = await client.pathUnchecked(nextLink).get(); + checkPagingRequest(result); + nextLink = getNextLink(result.body, options); + values = getElements(result.body, options); + yield values; + } +} + +/** + * Checks if a request failed + */ +function checkPagingRequest(response: PathUncheckedResponse) { + if (!Http2xxStatusCodes.includes(response.status)) { + throw createRestError( + `Pagination failed with unexpected statusCode ${response.status}`, + response + ); + } +} + +/** + * Gets for the value of nextLink in the body. If a custom nextLinkName was provided, it will be used instead of default + */ +function getNextLink(body: Record, paginateOptions: PaginateOptions = {}) { + const nextLinkName = paginateOptions.nextLinkName ?? DEFAULT_NEXTLINK; + const nextLink = body[nextLinkName]; + + if (typeof nextLink !== "string" && typeof nextLink !== "undefined") { + throw new Error(`Body Property ${nextLinkName} should be a string or undefined`); + } + + return nextLink; +} + +/** + * Gets the elements of the current request in the body. By default it will look in the `value` property unless + * a different value for itemName has been provided as part of the options. + */ +function getElements( + body: Record, + paginateOptions: PaginateOptions = {} +): T[] { + const valueName = paginateOptions?.itemName ?? DEFAULT_VALUES; + const value = body[valueName]; + + if (!Array.isArray(value)) { + throw new Error(`Body Property ${valueName} is not an array`); + } + + return (value as T[]) ?? []; +} diff --git a/sdk/core/core-client-paging-rest/src/url.browser.ts b/sdk/core/core-client-paging-rest/src/url.browser.ts new file mode 100644 index 000000000000..a6b3956caf41 --- /dev/null +++ b/sdk/core/core-client-paging-rest/src/url.browser.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// + +const url = URL; +const urlSearchParams = URLSearchParams; + +export { url as URL, urlSearchParams as URLSearchParams }; diff --git a/sdk/core/core-client-paging-rest/src/url.ts b/sdk/core/core-client-paging-rest/src/url.ts new file mode 100644 index 000000000000..993e69798f9e --- /dev/null +++ b/sdk/core/core-client-paging-rest/src/url.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export { URL, URLSearchParams } from "url"; diff --git a/sdk/core/core-client-paging-rest/test/paginate.spec.ts b/sdk/core/core-client-paging-rest/test/paginate.spec.ts new file mode 100644 index 000000000000..28404e7975cc --- /dev/null +++ b/sdk/core/core-client-paging-rest/test/paginate.spec.ts @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { Client, getClient, PathUncheckedResponse } from "@azure-rest/core-client"; +import { paginateResponse } from "../src/paginate"; +import { PipelineResponse, createHttpHeaders } from "@azure/core-rest-pipeline"; +import { URL } from "../src/url"; +import { PagedAsyncIterableIterator } from "@azure/core-paging"; + +/** + * This is a sample of how code generator can generate code around the Swagger spec for pagination to improve UX + */ + +// Helper type to extract the type of an array +type GetArrayType = T extends Array ? TData : unknown; + +// Helper type to infer the Type of the paged elements from the response type +// This type will be generated based on the swagger information for x-ms-pageable +// specifically on the itemName property which indicates the property of the response +// where the page items are found. The default value is `value` +export type PaginateReturn = TResult extends + | { + body: { value: infer TPage }; + } + | { + // In the tests below we are using values as a custom pagination property + // In cases like this the generator will have to generate one of these + // entries for each unique value of itemName in the swagger. Most of the times + // the itemName remains constant throughout the swagger, but that is not a requirement + body: { values: infer TPage }; + } + ? GetArrayType + : Array; + +/** + * Shapes of the test responses + */ +interface TestItem { + foo?: number; +} + +interface TestResponse extends PathUncheckedResponse { + body: { + value: Array; + }; +} + +interface TestResponseValues extends PathUncheckedResponse { + body: { + values: Array; + }; +} + +/** + * This is the default paginate helper function + */ +export function paginate( + client: Client, + initialResponse: TReturn +): PagedAsyncIterableIterator, PaginateReturn[]> { + return paginateResponse>(client, initialResponse); +} + +/** + * Paginate helper function defining a custom property to find the paged elements. + */ +export function paginateCustom( + client: Client, + initialResponse: TReturn +): PagedAsyncIterableIterator, PaginateReturn[]> { + // The generator would generate this based on the swagger so that our users don't need to specify the itemName + // when it can be taken from the swagger + return paginateResponse>(client, initialResponse, { itemName: "values" }); +} + +describe("Paginate heleper", () => { + let client: Client; + + beforeEach(() => { + client = getClient("http://localhost:3000", { allowInsecureConnection: true }); + client.pipeline.getOrderedPolicies().forEach(({ name }) => { + client.pipeline.removePolicy({ name }); + }); + }); + + it("Paging_getNoItemNamePages", async () => { + // Paginate assumes the resource supports get and nextLink is an opaque url to which a get can be done + // by default and following autorest x-ms-pageable extension, Paginate assumes that the pageable result + // will contain a property nextLink which is the opaque url for the next page, and a value property containing + // an array with the results (the page); + const expectedPage = [{ foo: 1 }]; + mockResponse(client, [ + { path: "/paging/noitemname", response: { status: 200, body: { value: expectedPage } } }, + ]); + const response: TestResponse = await client.pathUnchecked("/paging/noitemname").get(); + const items = paginate(client, response); + const result = []; + + for await (const item of items) { + result.push(item); + } + + assert.deepEqual(result, expectedPage); + }); + + it("Paging_getNullNextLinkNamePages", async () => { + // A paging operation that must ignore any kind of nextLink, and stop after page 1. + + const expectedPage = [{ foo: 1 }]; + mockResponse(client, [ + { + path: "/paging/nullnextlink", + response: { status: 200, body: { value: expectedPage, nextLink: "/paging/nullnextlink" } }, + }, + { + path: "/paging/nullnextlink", + response: { status: 400, body: { value: expectedPage, nextLink: "/paging/nullnextlink" } }, + }, + ]); + + const response: TestResponse = await client.pathUnchecked("/paging/nullnextlink").get(); + const items = paginateResponse(client, response, { nextLinkName: null }); + const result = []; + + for await (const item of items) { + result.push(item); + } + + assert.deepEqual(result, expectedPage); + }); + + it("Paging_getSinglePages", async () => { + // Autorest x-ms-pageable extension allows setting a different name for the property that contains the page + // we can allow overriding this through the pagingOptions values. + // The extension also allows setting a custom nextLink property name. + + const expectedPage = [{ foo: 1 }]; + mockResponse(client, [ + { path: "/paging/single", response: { status: 200, body: { values: expectedPage } } }, + ]); + + const response: TestResponseValues = await client.pathUnchecked("/paging/single").get(); + const items = paginateCustom(client, response); + const result = []; + for await (const item of items) { + // We get a strong type for item :) + result.push(item); + } + + assert.deepEqual(result, expectedPage); + }); + + it("Paging_firstResponseEmpty", async () => { + // First response has an empty [] next page contains a page with an element + const expectedPage = [{ foo: 1 }]; + mockResponse(client, [ + { + path: "/paging/firstResponseEmpty/1", + response: { status: 200, body: { value: [], nextLink: "/paging/firstResponseEmpty/2" } }, + }, + { + path: "/paging/firstResponseEmpty/2", + response: { status: 200, body: { value: expectedPage } }, + }, + ]); + + const response: TestResponse = await client.pathUnchecked("/paging/firstResponseEmpty/1").get(); + const items = paginate(client, response); + const result = []; + for await (const item of items) { + result.push(item); + } + + assert.deepEqual(result, expectedPage); + }); + + it("Paging_getMultiplePages", async () => { + const expectedPages = [{ foo: 1 }, { foo: 2 }, { foo: 3 }]; + + const mockResponses: MockResponse[] = [ + { + path: "/paging/multiple", + response: { + status: 200, + body: { value: [expectedPages[0]], nextLink: "/paging/multiple/1" }, + }, + }, + { + path: "/paging/multiple/1", + response: { + status: 200, + body: { value: [expectedPages[1]], nextLink: "/paging/multiple/2" }, + }, + }, + { + path: "/paging/multiple/2", + response: { + status: 200, + body: { value: [expectedPages[2]], nextLink: undefined }, + }, + }, + ]; + + mockResponse(client, mockResponses); + + const response: TestResponse = await client.pathUnchecked("/paging/multiple").get(); + const items = paginate(client, response); + const result = []; + for await (const item of items) { + result.push(item); + } + + assert.deepEqual(result, [...expectedPages]); + }); +}); + +interface MockResponse { + path: string; + response: { + status: number; + body: any; + }; +} + +/** + * Creates a pipeline with a mocked service call + * @param client - client to mock requests for + * @param response - Responses to return, the actual request url is matched to one of the paths in the responses and the defined object is returned. + * if no path matches a 404 error is returned + */ +function mockResponse(client: Client, responses: MockResponse[]) { + let count = 0; + + client.pipeline.addPolicy({ + name: "mockClient", + sendRequest: async (request, _next): Promise => { + if (count < responses.length) { + count++; + } + + const path = new URL(request.url).pathname; + + const response = responses.find((r) => r.path === path); + + if (!response) { + return { + headers: createHttpHeaders(), + request, + status: 404, + }; + } + + const { body, status } = response.response; + const bodyAsText = JSON.stringify(body); + return { + headers: createHttpHeaders(), + request, + status, + bodyAsText, + }; + }, + }); +} diff --git a/sdk/core/core-client-paging-rest/tsconfig.json b/sdk/core/core-client-paging-rest/tsconfig.json new file mode 100644 index 000000000000..82e643af7e8c --- /dev/null +++ b/sdk/core/core-client-paging-rest/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.package", + "compilerOptions": { + "outDir": "./dist-esm", + "declarationDir": "./types/latest" + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/sdk/core/core-client-paging-rest/tsdoc.json b/sdk/core/core-client-paging-rest/tsdoc.json new file mode 100644 index 000000000000..81c5a8a2aa2f --- /dev/null +++ b/sdk/core/core-client-paging-rest/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../../tsdoc.json"] +} diff --git a/sdk/core/core-client-rest/CHANGELOG.md b/sdk/core/core-client-rest/CHANGELOG.md index a1db200e7f80..ec7cbfad0df2 100644 --- a/sdk/core/core-client-rest/CHANGELOG.md +++ b/sdk/core/core-client-rest/CHANGELOG.md @@ -1,4 +1,11 @@ -# Release History +# Release History\ + +## 1.0.0-beta.5 (2021-06-24) + +### Features Added + +- Expose client option to set `allowInsecureConnection` to support http. [#15831](https://github.com/Azure/azure-sdk-for-js/pull/15831) +- Add new createRestError which takes a response to create a RestError. [#15831](https://github.com/Azure/azure-sdk-for-js/pull/15831) ## 1.0.0-beta.4 (2021-05-27) diff --git a/sdk/core/core-client-rest/api-extractor.json b/sdk/core/core-client-rest/api-extractor.json index f2f292d5bd5d..26337c79ce43 100644 --- a/sdk/core/core-client-rest/api-extractor.json +++ b/sdk/core/core-client-rest/api-extractor.json @@ -1,6 +1,6 @@ { "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "mainEntryPointFilePath": "types/src/index.d.ts", + "mainEntryPointFilePath": "types/latest/src/index.d.ts", "docModel": { "enabled": true }, @@ -11,7 +11,7 @@ "dtsRollup": { "enabled": true, "untrimmedFilePath": "", - "publicTrimmedFilePath": "./types/src/latest/core-client-rest.d.ts" + "publicTrimmedFilePath": "./types/latest/core-client-rest.d.ts" }, "messages": { "tsdocMessageReporting": { diff --git a/sdk/core/core-client-rest/karma.conf.js b/sdk/core/core-client-rest/karma.conf.js index 346ad9e087da..62b9c70ebd81 100644 --- a/sdk/core/core-client-rest/karma.conf.js +++ b/sdk/core/core-client-rest/karma.conf.js @@ -85,7 +85,13 @@ module.exports = function (config) { // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher // 'ChromeHeadless', 'Chrome', 'Firefox', 'Edge', 'IE' - browsers: ["ChromeHeadless"], + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits diff --git a/sdk/core/core-client-rest/package.json b/sdk/core/core-client-rest/package.json index 8df299483348..c7e5cfc07697 100644 --- a/sdk/core/core-client-rest/package.json +++ b/sdk/core/core-client-rest/package.json @@ -1,6 +1,6 @@ { "name": "@azure-rest/core-client", - "version": "1.0.0-beta.4", + "version": "1.0.0-beta.5", "description": "Core library for interfacing with AutoRest rest level generated code", "sdk-type": "client", "main": "dist/index.js", @@ -8,7 +8,7 @@ "browser": { "./dist-esm/src/url.js": "./dist-esm/src/url.browser.js" }, - "types": "types/src/latest/core-client-rest.d.ts", + "types": "types/latest/core-client-rest.d.ts", "scripts": { "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", "build:browser": "npm run build:ts && cross-env ONLY_BROWSER=true rollup -c 2>&1", diff --git a/sdk/core/core-client-rest/review/core-client.api.md b/sdk/core/core-client-rest/review/core-client.api.md index ea051b9894bf..a853c4d6ef32 100644 --- a/sdk/core/core-client-rest/review/core-client.api.md +++ b/sdk/core/core-client-rest/review/core-client.api.md @@ -9,6 +9,7 @@ import { Pipeline } from '@azure/core-rest-pipeline'; import { PipelineOptions } from '@azure/core-rest-pipeline'; import { PipelineRequest } from '@azure/core-rest-pipeline'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; +import { RestError } from '@azure/core-rest-pipeline'; import { TokenCredential } from '@azure/core-auth'; // @public @@ -41,13 +42,17 @@ export type ClientOptions = PipelineOptions & { }; baseUrl?: string; apiVersion?: string; + allowInsecureConnection?: boolean; }; // @public export function createDefaultPipeline(baseUrl: string, credential?: TokenCredential | KeyCredential, options?: ClientOptions): Pipeline; // @public -export function getClient(baseUrl: string, options?: PipelineOptions): Client; +export function createRestError(message: string, response: PathUncheckedResponse): RestError; + +// @public +export function getClient(baseUrl: string, options?: ClientOptions): Client; // @public export function getClient(baseUrl: string, credentials?: TokenCredential | KeyCredential, options?: ClientOptions): Client; @@ -79,7 +84,11 @@ export type RequestParameters = { }; // @public -export type RouteParams = TRoute extends `{${infer _Param}}/${infer Tail}` ? [pathParam: string, ...pathParams: RouteParams] : TRoute extends `{${infer _Param}}` ? [pathParam: string] : TRoute extends `${infer _Prefix}:${infer Tail}` ? RouteParams<`{${Tail}}`> : []; +export type RouteParams = TRoute extends `${infer _Head}/{${infer _Param}}${infer Tail}` ? [ + pathParam: string, + ...pathParams: RouteParams +] : [ +]; ``` diff --git a/sdk/core/core-client-rest/src/common.ts b/sdk/core/core-client-rest/src/common.ts index 432ec7179601..c51d5e5e0653 100644 --- a/sdk/core/core-client-rest/src/common.ts +++ b/sdk/core/core-client-rest/src/common.ts @@ -28,6 +28,10 @@ export type ClientOptions = PipelineOptions & { * Options for setting a custom apiVersion. */ apiVersion?: string; + /** + * Option to allow calling http (insecure) endpoints + */ + allowInsecureConnection?: boolean; }; /** diff --git a/sdk/core/core-client-rest/src/getClient.ts b/sdk/core/core-client-rest/src/getClient.ts index a26613319857..69e8b5e23c60 100644 --- a/sdk/core/core-client-rest/src/getClient.ts +++ b/sdk/core/core-client-rest/src/getClient.ts @@ -52,7 +52,7 @@ export interface Client { * @param baseUrl - Base endpoint for the client * @param options - Client options */ -export function getClient(baseUrl: string, options?: PipelineOptions): Client; +export function getClient(baseUrl: string, options?: ClientOptions): Client; /** * Creates a client with a default pipeline * @param baseUrl - Base endpoint for the client @@ -70,7 +70,6 @@ export function getClient( clientOptions: ClientOptions = {} ): Client { let credentials: TokenCredential | KeyCredential | undefined; - if (credentialsOrPipelineOptions) { if (isCredential(credentialsOrPipelineOptions)) { credentials = credentialsOrPipelineOptions; @@ -80,31 +79,96 @@ export function getClient( } const pipeline = createDefaultPipeline(baseUrl, credentials, clientOptions); + const { allowInsecureConnection } = clientOptions; const client = (path: string, ...args: Array) => { return { get: (options: RequestParameters = {}): Promise => { - return buildSendRequest("GET", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "GET", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, post: (options: RequestParameters = {}): Promise => { - return buildSendRequest("POST", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "POST", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, put: (options: RequestParameters = {}): Promise => { - return buildSendRequest("PUT", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "PUT", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, patch: (options: RequestParameters = {}): Promise => { - return buildSendRequest("PATCH", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "PATCH", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, delete: (options: RequestParameters = {}): Promise => { - return buildSendRequest("DELETE", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "DELETE", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, head: (options: RequestParameters = {}): Promise => { - return buildSendRequest("HEAD", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "HEAD", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, options: (options: RequestParameters = {}): Promise => { - return buildSendRequest("OPTIONS", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "OPTIONS", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, trace: (options: RequestParameters = {}): Promise => { - return buildSendRequest("TRACE", clientOptions, baseUrl, path, pipeline, options, args); + return buildSendRequest( + "TRACE", + clientOptions, + baseUrl, + path, + pipeline, + { allowInsecureConnection, ...options }, + args + ); }, }; }; diff --git a/sdk/core/core-client-rest/src/index.ts b/sdk/core/core-client-rest/src/index.ts index ea2e7c612dcf..6835b4e377bb 100644 --- a/sdk/core/core-client-rest/src/index.ts +++ b/sdk/core/core-client-rest/src/index.ts @@ -8,6 +8,7 @@ export { createDefaultPipeline } from "./clientHelpers"; export { CertificateCredential, isCertificateCredential } from "./certificateCredential"; +export { createRestError } from "./restError"; export * from "./common"; export * from "./getClient"; export * from "./pathClientTypes"; diff --git a/sdk/core/core-client-rest/src/pathClientTypes.ts b/sdk/core/core-client-rest/src/pathClientTypes.ts index 75dfe73f43fe..03cb7ff8d47f 100644 --- a/sdk/core/core-client-rest/src/pathClientTypes.ts +++ b/sdk/core/core-client-rest/src/pathClientTypes.ts @@ -37,10 +37,22 @@ export type RequestParameters = { * Helper type used to detect parameters in a path template * keys surounded by \{\} will be considered a path parameter */ -export type RouteParams = TRoute extends `{${infer _Param}}/${infer Tail}` - ? [pathParam: string, ...pathParams: RouteParams] - : TRoute extends `{${infer _Param}}` - ? [pathParam: string] - : TRoute extends `${infer _Prefix}:${infer Tail}` - ? RouteParams<`{${Tail}}`> - : []; +export type RouteParams< + TRoute extends string + // This is trying to match the string in TRoute with a template where HEAD/{PARAM}/TAIL + // for example in the followint path: /foo/{fooId}/bar/{barId}/baz the template will infer + // HEAD: /foo + // Param: fooId + // Tail: /bar/{barId}/baz + // The above sample path would return [pathParam: string, pathParam: string] +> = TRoute extends `${infer _Head}/{${infer _Param}}${infer Tail}` + ? // In case we have a match for the template above we know for sure + // that we have at least one pathParameter, that's why we set the first pathParam + // in the tuple. At this point we have only matched up until param, if we want to identify + // additional parameters we can call RouteParameters recursively on the Tail to match the remaining parts, + // in case the Tail has more parameters, it will return a tuple with the parameters found in tail. + // We spread the second path params to end up with a single dimension tuple at the end. + [pathParam: string, ...pathParams: RouteParams] + : // When the path doesn't match the template, it means that we have no path parameters so we return + // an empty tuple. + []; diff --git a/sdk/core/core-client-rest/src/restError.ts b/sdk/core/core-client-rest/src/restError.ts new file mode 100644 index 000000000000..1cb18be69502 --- /dev/null +++ b/sdk/core/core-client-rest/src/restError.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { PathUncheckedResponse } from "./getClient"; +import { RestError, PipelineResponse, createHttpHeaders } from "@azure/core-rest-pipeline"; + +/** + * Creates a rest error from a PathUnchecked response + */ +export function createRestError(message: string, response: PathUncheckedResponse): RestError { + return new RestError(message, { + statusCode: statusCodeToNumber(response.status), + request: response.request, + response: toPipelineResponse(response), + }); +} + +function toPipelineResponse(response: PathUncheckedResponse): PipelineResponse { + return { + headers: createHttpHeaders(response.headers), + request: response.request, + status: statusCodeToNumber(response.status) ?? -1, + }; +} + +function statusCodeToNumber(statusCode: string): number | undefined { + const status = Number.parseInt(statusCode); + + return Number.isNaN(status) ? undefined : status; +} diff --git a/sdk/core/core-client-rest/tsconfig.json b/sdk/core/core-client-rest/tsconfig.json index 3863167ddb92..82e643af7e8c 100644 --- a/sdk/core/core-client-rest/tsconfig.json +++ b/sdk/core/core-client-rest/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../../tsconfig.package", "compilerOptions": { "outDir": "./dist-esm", - "declarationDir": "./types" + "declarationDir": "./types/latest" }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/sdk/documenttranslator/ai-document-translator-rest/package.json b/sdk/documenttranslator/ai-document-translator-rest/package.json index 85dc02526b4e..ae9275a2b86d 100644 --- a/sdk/documenttranslator/ai-document-translator-rest/package.json +++ b/sdk/documenttranslator/ai-document-translator-rest/package.json @@ -90,7 +90,7 @@ "autoPublish": false, "dependencies": { "@azure/core-auth": "^1.3.0", - "@azure-rest/core-client": "1.0.0-beta.4", + "@azure-rest/core-client": "1.0.0-beta.5", "@azure/core-rest-pipeline": "^1.1.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" diff --git a/sdk/purview/purview-catalog-rest/package.json b/sdk/purview/purview-catalog-rest/package.json index c26b4d53f52e..4ceda593c3db 100644 --- a/sdk/purview/purview-catalog-rest/package.json +++ b/sdk/purview/purview-catalog-rest/package.json @@ -84,7 +84,7 @@ "autoPublish": false, "dependencies": { "@azure/core-auth": "^1.3.0", - "@azure-rest/core-client": "1.0.0-beta.4", + "@azure-rest/core-client": "1.0.0-beta.5", "@azure/core-rest-pipeline": "^1.1.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" diff --git a/sdk/purview/purview-scanning-rest/package.json b/sdk/purview/purview-scanning-rest/package.json index 61584a41ab83..dd3258b10eaa 100644 --- a/sdk/purview/purview-scanning-rest/package.json +++ b/sdk/purview/purview-scanning-rest/package.json @@ -84,7 +84,7 @@ "autoPublish": false, "dependencies": { "@azure/core-auth": "^1.3.0", - "@azure-rest/core-client": "1.0.0-beta.4", + "@azure-rest/core-client": "1.0.0-beta.5", "@azure/core-rest-pipeline": "^1.1.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0"