From cca6a2ae438c907158dcfd34d4db9e8925ca7437 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 11 Jun 2021 16:34:27 -0700 Subject: [PATCH 01/11] Prototype paging helper function --- sdk/core/core-client-rest/package.json | 3 +- sdk/core/core-client-rest/src/common.ts | 4 + sdk/core/core-client-rest/src/getClient.ts | 85 +++++++++++++++--- sdk/core/core-client-rest/src/paginate.ts | 88 +++++++++++++++++++ .../core-client-rest/test/paginate.spec.ts | 59 +++++++++++++ 5 files changed, 228 insertions(+), 11 deletions(-) create mode 100644 sdk/core/core-client-rest/src/paginate.ts create mode 100644 sdk/core/core-client-rest/test/paginate.spec.ts diff --git a/sdk/core/core-client-rest/package.json b/sdk/core/core-client-rest/package.json index 8df299483348..2a3506c1855f 100644 --- a/sdk/core/core-client-rest/package.json +++ b/sdk/core/core-client-rest/package.json @@ -61,6 +61,7 @@ "sideEffects": false, "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", "dependencies": { + "@azure/core-paging": "^1.1.1", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.1.0", "tslib": "^2.2.0" @@ -97,4 +98,4 @@ "util": "^0.12.1", "typedoc": "0.15.2" } -} +} \ No newline at end of file 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..2f3b307bf90d 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; @@ -81,30 +80,96 @@ export function getClient( const pipeline = createDefaultPipeline(baseUrl, credentials, clientOptions); const client = (path: string, ...args: Array) => { + const { allowInsecureConnection } = clientOptions; + 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/paginate.ts b/sdk/core/core-client-rest/src/paginate.ts new file mode 100644 index 000000000000..94fd00bcca3a --- /dev/null +++ b/sdk/core/core-client-rest/src/paginate.ts @@ -0,0 +1,88 @@ +/// + +import { Client, PathUncheckedResponse } from "./getClient"; +import "@azure/core-paging"; +import { PagedAsyncIterableIterator } from "@azure/core-paging"; + +const DEFAULT_NEXTLINK = "nextLink"; +const DEFAULT_VALUES = "value"; + +interface PaginateOptions { + nextLinkName?: string; + valuesName?: string; +} + +export function paginate>( + client: Client, + path: string, + paginateOptions: PaginateOptions = {} +): PagedAsyncIterableIterator { + const iter = listAll(client, path, paginateOptions); + return { + next() { + return iter.next(); + }, + [Symbol.asyncIterator]() { + return this; + }, + byPage: () => { + return listPage(client, path, paginateOptions); + }, + }; +} + +async function* listAll( + client: Client, + path: string, + paginateOptions: PaginateOptions +): AsyncIterableIterator { + for await (const page of listPage(client, path, paginateOptions)) { + yield* page; + } +} + +async function* listPage[]>( + client: Client, + path: string, + paginateOptions: PaginateOptions +): AsyncIterableIterator { + let result = await client.pathUnchecked(path).get(); + checkPagingRequest(result); + let nextLink = getNextLink(result.body, paginateOptions); + let values = getElements(result.body, paginateOptions); + + yield values; + + while (nextLink) { + result = await client.pathUnchecked(nextLink).get(); + checkPagingRequest(result); + nextLink = getNextLink(result.body, paginateOptions); + values = getElements(result.body, paginateOptions); + yield values; + } +} + +function checkPagingRequest(response: PathUncheckedResponse) { + if (!["200", "201", "204"].includes(response.status)) { + throw ( + response.body?.error ?? { + message: `Pagination failed`, + request: response.request, + response: response, + status: response.status, + } + ); + } +} + +function getNextLink(body: Record, paginateOptions: PaginateOptions) { + const nextLinkName = paginateOptions.nextLinkName ?? DEFAULT_NEXTLINK; + return (body[nextLinkName] as string) ?? undefined; +} +function getElements( + body: Record, + paginateOptions: PaginateOptions +): T[] { + const valueName = paginateOptions?.valuesName ?? DEFAULT_VALUES; + return (body[valueName] as T[]) ?? []; +} diff --git a/sdk/core/core-client-rest/test/paginate.spec.ts b/sdk/core/core-client-rest/test/paginate.spec.ts new file mode 100644 index 000000000000..bce2f965f415 --- /dev/null +++ b/sdk/core/core-client-rest/test/paginate.spec.ts @@ -0,0 +1,59 @@ +import { assert } from "chai"; +import { Client, getClient } from "../src"; +import { paginate } from "../src/paginate"; + +describe.only("Paginate heleper", () => { + let client: Client; + + beforeEach(() => { + client = getClient("http://localhost:3000", { allowInsecureConnection: true }); + }); + + 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 items = paginate(client, "/paging/noitemname"); + let result = []; + for await (const item of items) { + result.push(item); + } + + assert.lengthOf(result, 1); + }); + + 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 items = paginate(client, "/paging/single", { valuesName: "values" }); + let result = []; + for await (const item of items) { + result.push(item); + } + + assert.lengthOf(result, 1); + }); + + it("Paging_firstResponseEmpty", async () => { + // First response has an empty [] next page contains a page with an element + const items = paginate(client, "/paging/firstResponseEmpty/1"); + let result = []; + for await (const item of items) { + result.push(item); + } + + assert.lengthOf(result, 1); + }); + + it("Paging_getMultiplePages", async () => { + const items = paginate(client, "/paging/multiple", { valuesName: "values" }); + let result = []; + for await (const item of items) { + result.push(item); + } + + assert.lengthOf(result, 10); + }); +}); From b8ef44f845125bb780f90914419a46349c9f48ad Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Sat, 12 Jun 2021 16:32:19 -0700 Subject: [PATCH 02/11] Use paginate in farmbeats --- .../agrifood-farming-rest/package.json | 1 + .../review/agrifood-farming.api.md | 6 ++++ .../samples-dev/listFarmers.ts | 31 +++---------------- .../agrifood-farming-rest/src/farmBeats.ts | 25 ++++++++++++++- .../review/core-client.api.md | 9 +++++- sdk/core/core-client-rest/src/index.ts | 1 + sdk/core/core-client-rest/src/paginate.ts | 10 +++--- 7 files changed, 50 insertions(+), 33 deletions(-) diff --git a/sdk/agrifood/agrifood-farming-rest/package.json b/sdk/agrifood/agrifood-farming-rest/package.json index 0c27893927cf..19dbd2c6f665 100644 --- a/sdk/agrifood/agrifood-farming-rest/package.json +++ b/sdk/agrifood/agrifood-farming-rest/package.json @@ -86,6 +86,7 @@ "dependencies": { "@azure/core-auth": "^1.3.0", "@azure-rest/core-client": "1.0.0-beta.4", + "@azure/core-paging": "^1.1.1", "@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..bbcedeacfae2 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,7 @@ 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/core-paging'; import { RequestParameters } from '@azure-rest/core-client'; import { TokenCredential } from '@azure/core-auth'; @@ -2589,6 +2590,11 @@ export interface OAuthTokensListQueryParamProperties { minLastModifiedDateTime?: Date; } +// Warning: (ae-forgotten-export) The symbol "PageableRoutes" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function paginate(client: Client, path: TPath, options?: any): PagedAsyncIterableIterator; + // @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..ca807a6efcc2 100644 --- a/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts +++ b/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts @@ -8,7 +8,8 @@ * @azsdk-weight 20 */ -import FarmBeats, { Farmer } from "@azure-rest/agrifood-farming"; +import FarmBeats, { Farmer, paginate } from "@azure-rest/agrifood-farming"; +import { PagedAsyncIterableIterator } from "@azure/core-paging"; import { DefaultAzureCredential } from "@azure/identity"; import dotenv from "dotenv"; @@ -18,34 +19,12 @@ const endpoint = process.env["FARMBEATS_ENDPOINT"] || ""; async function main() { const farming = FarmBeats(endpoint, new DefaultAzureCredential()); - - const result = await farming.path("/farmers").get(); - - if (result.status !== "200") { - throw result.body.error?.message; - } - - 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: PagedAsyncIterableIterator = paginate(farming, "/farmers"); // 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/farmBeats.ts b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts index 9e439797a8e2..f3e77ae2fca5 100644 --- a/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts +++ b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts @@ -306,8 +306,15 @@ import { WeatherCreateDataDeleteJob202Response, WeatherCreateDataDeleteJobdefaultResponse, } from "./responses"; -import { getClient, ClientOptions, Client } from "@azure-rest/core-client"; +import { + getClient, + ClientOptions, + Client, + paginate as corePaginate, +} from "@azure-rest/core-client"; import { TokenCredential } from "@azure/core-auth"; +import { PagedAsyncIterableIterator } from "@azure/core-paging"; +import { Farmer } from "./models"; export interface ApplicationDataListByFarmerId { /** Returns a paginated list of application data resources under a particular farm. */ @@ -1110,6 +1117,22 @@ export type FarmBeatsRestClient = Client & { path: Routes; }; +interface PageableRoutes { + "/farmers": Farmer; +} + +export function paginate( + client: Client, + path: TPath, + options?: any +): PagedAsyncIterableIterator { + // In case a service needs a custom pagination logic, we could tell the generator + // to import from a different location instead of core-client. + // For example if we wanted to have an src/extensions/pagination.ts with a custom + // paginate implementation that follows the corePaginate type definition. + return corePaginate(client, path, options); +} + export default function FarmBeats( Endpoint: string, credentials: TokenCredential, 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..30a7fb2886c1 100644 --- a/sdk/core/core-client-rest/review/core-client.api.md +++ b/sdk/core/core-client-rest/review/core-client.api.md @@ -5,6 +5,7 @@ ```ts import { KeyCredential } from '@azure/core-auth'; +import { PagedAsyncIterableIterator } from '@azure/core-paging'; import { Pipeline } from '@azure/core-rest-pipeline'; import { PipelineOptions } from '@azure/core-rest-pipeline'; import { PipelineRequest } from '@azure/core-rest-pipeline'; @@ -41,13 +42,14 @@ 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 getClient(baseUrl: string, options?: ClientOptions): Client; // @public export function getClient(baseUrl: string, credentials?: TokenCredential | KeyCredential, options?: ClientOptions): Client; @@ -63,6 +65,11 @@ export type HttpResponse = { // @public export function isCertificateCredential(credential: unknown): credential is CertificateCredential; +// Warning: (ae-forgotten-export) The symbol "PaginateOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function paginate>(client: Client, path: TPath, paginateOptions?: PaginateOptions): PagedAsyncIterableIterator; + // @public export type PathUncheckedResponse = HttpResponse & { body: any; diff --git a/sdk/core/core-client-rest/src/index.ts b/sdk/core/core-client-rest/src/index.ts index ea2e7c612dcf..90b055467bc0 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 { paginate } from "./paginate"; export * from "./common"; export * from "./getClient"; export * from "./pathClientTypes"; diff --git a/sdk/core/core-client-rest/src/paginate.ts b/sdk/core/core-client-rest/src/paginate.ts index 94fd00bcca3a..91022a4b7608 100644 --- a/sdk/core/core-client-rest/src/paginate.ts +++ b/sdk/core/core-client-rest/src/paginate.ts @@ -12,12 +12,12 @@ interface PaginateOptions { valuesName?: string; } -export function paginate>( +export function paginate>( client: Client, - path: string, + path: TPath, paginateOptions: PaginateOptions = {} -): PagedAsyncIterableIterator { - const iter = listAll(client, path, paginateOptions); +): PagedAsyncIterableIterator { + const iter = listAll(client, path, paginateOptions); return { next() { return iter.next(); @@ -26,7 +26,7 @@ export function paginate>( return this; }, byPage: () => { - return listPage(client, path, paginateOptions); + return listPage(client, path, paginateOptions); }, }; } From 3c566e72eff3470ce9f72d6f66c15063a6ecad14 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Thu, 17 Jun 2021 15:49:31 -0700 Subject: [PATCH 03/11] Updates --- sdk/core/core-client-rest/README.md | 48 ++++ sdk/core/core-client-rest/karma.conf.js | 8 +- sdk/core/core-client-rest/package.json | 2 +- .../review/core-client.api.md | 10 +- sdk/core/core-client-rest/src/index.ts | 2 +- sdk/core/core-client-rest/src/paginate.ts | 81 +++++-- .../core-client-rest/test/paginate.spec.ts | 209 ++++++++++++++++-- 7 files changed, 316 insertions(+), 44 deletions(-) diff --git a/sdk/core/core-client-rest/README.md b/sdk/core/core-client-rest/README.md index 4cdcdbe50460..2f719883bd6c 100644 --- a/sdk/core/core-client-rest/README.md +++ b/sdk/core/core-client-rest/README.md @@ -14,6 +14,54 @@ This package is primarily used in generated code and not meant to be consumed di ## Key concepts +### 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 PagebleAsyncIterator 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 +/** + * 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 stronly 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); +} +``` + ## Examples Examples can be found in the `samples` folder. 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 2a3506c1855f..d9ad55259024 100644 --- a/sdk/core/core-client-rest/package.json +++ b/sdk/core/core-client-rest/package.json @@ -98,4 +98,4 @@ "util": "^0.12.1", "typedoc": "0.15.2" } -} \ No newline at end of file +} 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 30a7fb2886c1..5b9f4d6c0a6e 100644 --- a/sdk/core/core-client-rest/review/core-client.api.md +++ b/sdk/core/core-client-rest/review/core-client.api.md @@ -65,10 +65,14 @@ export type HttpResponse = { // @public export function isCertificateCredential(credential: unknown): credential is CertificateCredential; -// Warning: (ae-forgotten-export) The symbol "PaginateOptions" needs to be exported by the entry point index.d.ts -// +// @public +export interface PaginateOptions { + itemName?: string; + nextLinkName?: string; +} + // @public (undocumented) -export function paginate>(client: Client, path: TPath, paginateOptions?: PaginateOptions): PagedAsyncIterableIterator; +export function paginateResponse(client: Client, initialResponse: HttpResponse, options?: PaginateOptions): PagedAsyncIterableIterator; // @public export type PathUncheckedResponse = HttpResponse & { diff --git a/sdk/core/core-client-rest/src/index.ts b/sdk/core/core-client-rest/src/index.ts index 90b055467bc0..d24866b57ed7 100644 --- a/sdk/core/core-client-rest/src/index.ts +++ b/sdk/core/core-client-rest/src/index.ts @@ -8,7 +8,7 @@ export { createDefaultPipeline } from "./clientHelpers"; export { CertificateCredential, isCertificateCredential } from "./certificateCredential"; -export { paginate } from "./paginate"; +export { paginateResponse, PaginateOptions } from "./paginate"; export * from "./common"; export * from "./getClient"; export * from "./pathClientTypes"; diff --git a/sdk/core/core-client-rest/src/paginate.ts b/sdk/core/core-client-rest/src/paginate.ts index 91022a4b7608..d037bce0f8bd 100644 --- a/sdk/core/core-client-rest/src/paginate.ts +++ b/sdk/core/core-client-rest/src/paginate.ts @@ -1,23 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /// -import { Client, PathUncheckedResponse } from "./getClient"; +import { Client, HttpResponse, PathUncheckedResponse } from "./"; import "@azure/core-paging"; 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"; -interface PaginateOptions { +/** + * 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. + */ nextLinkName?: string; - valuesName?: string; + /** + * Indicates the name of the property in which the set of values is found. Default: `value` + */ + itemName?: string; } -export function paginate>( +/** + * + * @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, - path: TPath, - paginateOptions: PaginateOptions = {} -): PagedAsyncIterableIterator { - const iter = listAll(client, path, paginateOptions); + initialResponse: HttpResponse, + options: PaginateOptions = {} +): PagedAsyncIterableIterator { + const iter = listAll(client, initialResponse, options); return { next() { return iter.next(); @@ -26,44 +50,47 @@ export function paginate return this; }, byPage: () => { - return listPage(client, path, paginateOptions); + return listPage(client, initialResponse, options); }, }; } async function* listAll( client: Client, - path: string, + initialResponse: PathUncheckedResponse, paginateOptions: PaginateOptions ): AsyncIterableIterator { - for await (const page of listPage(client, path, paginateOptions)) { + for await (const page of listPage(client, initialResponse, paginateOptions)) { yield* page; } } -async function* listPage[]>( +async function* listPage[]>( client: Client, - path: string, - paginateOptions: PaginateOptions + initialResponse: PathUncheckedResponse, + options: PaginateOptions ): AsyncIterableIterator { - let result = await client.pathUnchecked(path).get(); + let result = initialResponse; checkPagingRequest(result); - let nextLink = getNextLink(result.body, paginateOptions); - let values = getElements(result.body, paginateOptions); + let nextLink = getNextLink(result.body, options); + let values = getElements(result.body, options); yield values; while (nextLink) { result = await client.pathUnchecked(nextLink).get(); checkPagingRequest(result); - nextLink = getNextLink(result.body, paginateOptions); - values = getElements(result.body, paginateOptions); + nextLink = getNextLink(result.body, options); + values = getElements(result.body, options); yield values; } } +/** + * Checks if a request failed + */ function checkPagingRequest(response: PathUncheckedResponse) { - if (!["200", "201", "204"].includes(response.status)) { + if (!Http2xxStatusCodes.includes(response.status)) { throw ( response.body?.error ?? { message: `Pagination failed`, @@ -75,14 +102,22 @@ function checkPagingRequest(response: PathUncheckedResponse) { } } -function getNextLink(body: Record, paginateOptions: PaginateOptions) { +/** + * 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; return (body[nextLinkName] as string) ?? undefined; } + +/** + * 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 + paginateOptions: PaginateOptions = {} ): T[] { - const valueName = paginateOptions?.valuesName ?? DEFAULT_VALUES; + const valueName = paginateOptions?.itemName ?? DEFAULT_VALUES; return (body[valueName] as T[]) ?? []; } diff --git a/sdk/core/core-client-rest/test/paginate.spec.ts b/sdk/core/core-client-rest/test/paginate.spec.ts index bce2f965f415..8c32974fa295 100644 --- a/sdk/core/core-client-rest/test/paginate.spec.ts +++ b/sdk/core/core-client-rest/test/paginate.spec.ts @@ -1,12 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + import { assert } from "chai"; -import { Client, getClient } from "../src"; -import { paginate } from "../src/paginate"; +import { Client, getClient, PathUncheckedResponse } from "../src"; +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; +} -describe.only("Paginate heleper", () => { +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 () => { @@ -14,46 +89,150 @@ describe.only("Paginate heleper", () => { // 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 items = paginate(client, "/paging/noitemname"); - let result = []; + 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.lengthOf(result, 1); + 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 items = paginate(client, "/paging/single", { valuesName: "values" }); - let result = []; + + 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.lengthOf(result, 1); + assert.deepEqual(result, expectedPage); }); it("Paging_firstResponseEmpty", async () => { // First response has an empty [] next page contains a page with an element - const items = paginate(client, "/paging/firstResponseEmpty/1"); - let result = []; + 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.lengthOf(result, 1); + assert.deepEqual(result, expectedPage); }); it("Paging_getMultiplePages", async () => { - const items = paginate(client, "/paging/multiple", { valuesName: "values" }); - let result = []; + 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.lengthOf(result, 10); + 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, + }; + }, + }); +} From 4ff68edb670460f15ed738dced9ddcfea5208a93 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Thu, 17 Jun 2021 17:24:04 -0700 Subject: [PATCH 04/11] Update farmbeats --- .../review/agrifood-farming.api.md | 17 ++++++-- .../samples-dev/listFarmers.ts | 11 +++-- .../agrifood-farming-rest/src/farmBeats.ts | 43 +++++++++++++------ .../agrifood-farming-rest/src/index.ts | 2 + .../test/public/smoke.spec.ts | 11 ++++- .../review/core-client.api.md | 4 +- sdk/core/core-client-rest/src/paginate.ts | 2 +- 7 files changed, 64 insertions(+), 26 deletions(-) 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 bbcedeacfae2..02a7a705dacd 100644 --- a/sdk/agrifood/agrifood-farming-rest/review/agrifood-farming.api.md +++ b/sdk/agrifood/agrifood-farming-rest/review/agrifood-farming.api.md @@ -8,6 +8,7 @@ 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/core-paging'; +import { PathUncheckedResponse } from '@azure-rest/core-client'; import { RequestParameters } from '@azure-rest/core-client'; import { TokenCredential } from '@azure/core-auth'; @@ -1922,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; @@ -2590,10 +2594,15 @@ export interface OAuthTokensListQueryParamProperties { minLastModifiedDateTime?: Date; } -// Warning: (ae-forgotten-export) The symbol "PageableRoutes" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export function paginate(client: Client, path: TPath, options?: any): PagedAsyncIterableIterator; +// @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 { diff --git a/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts b/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts index ca807a6efcc2..557886f7a5f8 100644 --- a/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts +++ b/sdk/agrifood/agrifood-farming-rest/samples-dev/listFarmers.ts @@ -8,8 +8,7 @@ * @azsdk-weight 20 */ -import FarmBeats, { Farmer, paginate } from "@azure-rest/agrifood-farming"; -import { PagedAsyncIterableIterator } from "@azure/core-paging"; +import FarmBeats, { paginate } from "@azure-rest/agrifood-farming"; import { DefaultAzureCredential } from "@azure/identity"; import dotenv from "dotenv"; @@ -19,7 +18,13 @@ const endpoint = process.env["FARMBEATS_ENDPOINT"] || ""; async function main() { const farming = FarmBeats(endpoint, new DefaultAzureCredential()); - const farmers: PagedAsyncIterableIterator = paginate(farming, "/farmers"); + const response = await farming.path("/farmers").get(); + + if (response.status !== "200") { + throw response.body.error || new Error(`Unexpected status code ${response.status}`); + } + + const farmers = paginate(farming, response); // Lof each farmer id for await (const farmer of farmers) { diff --git a/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts index f3e77ae2fca5..2580ebd96be3 100644 --- a/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts +++ b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts @@ -310,11 +310,11 @@ import { getClient, ClientOptions, Client, - paginate as corePaginate, + paginateResponse, + PathUncheckedResponse, } from "@azure-rest/core-client"; import { TokenCredential } from "@azure/core-auth"; import { PagedAsyncIterableIterator } from "@azure/core-paging"; -import { Farmer } from "./models"; export interface ApplicationDataListByFarmerId { /** Returns a paginated list of application data resources under a particular farm. */ @@ -1117,22 +1117,37 @@ export type FarmBeatsRestClient = Client & { path: Routes; }; -interface PageableRoutes { - "/farmers": Farmer; +/** + * 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; -export function paginate( +/** + * 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, - path: TPath, - options?: any -): PagedAsyncIterableIterator { - // In case a service needs a custom pagination logic, we could tell the generator - // to import from a different location instead of core-client. - // For example if we wanted to have an src/extensions/pagination.ts with a custom - // paginate implementation that follows the corePaginate type definition. - return corePaginate(client, path, options); + initialResponse: TReturn +): PagedAsyncIterableIterator, PaginateReturn[]> { + return paginateResponse>(client, initialResponse); } - export default function FarmBeats( Endpoint: string, credentials: TokenCredential, diff --git a/sdk/agrifood/agrifood-farming-rest/src/index.ts b/sdk/agrifood/agrifood-farming-rest/src/index.ts index 9a420561b89e..a77eb7c82796 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 "./farmBeats"; + export default FarmBeats; 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/core/core-client-rest/review/core-client.api.md b/sdk/core/core-client-rest/review/core-client.api.md index 5b9f4d6c0a6e..a9d452f43274 100644 --- a/sdk/core/core-client-rest/review/core-client.api.md +++ b/sdk/core/core-client-rest/review/core-client.api.md @@ -71,8 +71,8 @@ export interface PaginateOptions { nextLinkName?: string; } -// @public (undocumented) -export function paginateResponse(client: Client, initialResponse: HttpResponse, options?: PaginateOptions): PagedAsyncIterableIterator; +// @public +export function paginateResponse(client: Client, initialResponse: HttpResponse, options?: PaginateOptions): PagedAsyncIterableIterator; // @public export type PathUncheckedResponse = HttpResponse & { diff --git a/sdk/core/core-client-rest/src/paginate.ts b/sdk/core/core-client-rest/src/paginate.ts index d037bce0f8bd..c707241748cf 100644 --- a/sdk/core/core-client-rest/src/paginate.ts +++ b/sdk/core/core-client-rest/src/paginate.ts @@ -30,7 +30,7 @@ export interface PaginateOptions { } /** - * + * 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 From 79df903236a9b32d9c48f76a8e97913ac9a569c2 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 18 Jun 2021 11:21:38 -0700 Subject: [PATCH 05/11] Address PR feedback --- sdk/core/core-client-rest/README.md | 35 ++++++++++++++++++++-- sdk/core/core-client-rest/src/getClient.ts | 3 +- sdk/core/core-client-rest/src/paginate.ts | 16 ++++++++-- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/sdk/core/core-client-rest/README.md b/sdk/core/core-client-rest/README.md index 2f719883bd6c..cb11ca79f468 100644 --- a/sdk/core/core-client-rest/README.md +++ b/sdk/core/core-client-rest/README.md @@ -14,12 +14,14 @@ This package is primarily used in generated code and not meant to be consumed di ## Key concepts -### paginateResponse +### 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 PagebleAsyncIterator that can be used to get all the items or page by page. +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 @@ -29,7 +31,7 @@ In order to provide better typings, the library that consumes `paginateResponse` export function paginate( client: Client, initialResponse: TReturn -): PagedAsyncIterableIterator, PaginateReturn[], {}> { +): PagedAsyncIterableIterator, PaginateReturn[]> { return paginateResponse>(client, initialResponse, { // For example these values could come from the swagger itemName: "items", @@ -62,6 +64,33 @@ for await (const item of items) { } ``` +#### 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. diff --git a/sdk/core/core-client-rest/src/getClient.ts b/sdk/core/core-client-rest/src/getClient.ts index 2f3b307bf90d..69e8b5e23c60 100644 --- a/sdk/core/core-client-rest/src/getClient.ts +++ b/sdk/core/core-client-rest/src/getClient.ts @@ -79,9 +79,8 @@ export function getClient( } const pipeline = createDefaultPipeline(baseUrl, credentials, clientOptions); + const { allowInsecureConnection } = clientOptions; const client = (path: string, ...args: Array) => { - const { allowInsecureConnection } = clientOptions; - return { get: (options: RequestParameters = {}): Promise => { return buildSendRequest( diff --git a/sdk/core/core-client-rest/src/paginate.ts b/sdk/core/core-client-rest/src/paginate.ts index c707241748cf..e5c5d3c78597 100644 --- a/sdk/core/core-client-rest/src/paginate.ts +++ b/sdk/core/core-client-rest/src/paginate.ts @@ -107,7 +107,13 @@ function checkPagingRequest(response: PathUncheckedResponse) { */ function getNextLink(body: Record, paginateOptions: PaginateOptions = {}) { const nextLinkName = paginateOptions.nextLinkName ?? DEFAULT_NEXTLINK; - return (body[nextLinkName] as string) ?? undefined; + 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; } /** @@ -119,5 +125,11 @@ function getElements( paginateOptions: PaginateOptions = {} ): T[] { const valueName = paginateOptions?.itemName ?? DEFAULT_VALUES; - return (body[valueName] as T[]) ?? []; + const value = body[valueName]; + + if (!Array.isArray(value)) { + throw new Error(`Body Property ${valueName} is not an array`); + } + + return (value as T[]) ?? []; } From 49619af04df6453477cee11b41e98a0ec5d0aded Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 21 Jun 2021 11:29:26 -0700 Subject: [PATCH 06/11] Move paging to its own package --- rush.json | 5 + .../agrifood-farming-rest/package.json | 1 + .../agrifood-farming-rest/src/farmBeats.ts | 9 +- sdk/core/ci.yml | 2 + sdk/core/core-client-paging-rest/CHANGELOG.md | 5 + sdk/core/core-client-paging-rest/LICENSE | 21 +++ sdk/core/core-client-paging-rest/README.md | 112 ++++++++++++++++ .../api-extractor.json | 31 +++++ .../core-client-paging-rest/karma.conf.js | 121 ++++++++++++++++++ sdk/core/core-client-paging-rest/package.json | 101 +++++++++++++++ .../review/core-client-paging.api.md | 21 +++ .../core-client-paging-rest/rollup.config.js | 2 + sdk/core/core-client-paging-rest/src/index.ts | 9 ++ .../src/paginate.ts | 14 +- .../src/url.browser.ts | 9 ++ sdk/core/core-client-paging-rest/src/url.ts | 4 + .../test/paginate.spec.ts | 28 +++- .../core-client-paging-rest/tsconfig.json | 8 ++ sdk/core/core-client-paging-rest/tsdoc.json | 4 + sdk/core/core-client-rest/README.md | 77 ----------- .../review/core-client.api.md | 10 -- sdk/core/core-client-rest/src/index.ts | 1 - sdk/core/core-client-rest/tsconfig.json | 2 +- 23 files changed, 497 insertions(+), 100 deletions(-) create mode 100644 sdk/core/core-client-paging-rest/CHANGELOG.md create mode 100644 sdk/core/core-client-paging-rest/LICENSE create mode 100644 sdk/core/core-client-paging-rest/README.md create mode 100644 sdk/core/core-client-paging-rest/api-extractor.json create mode 100644 sdk/core/core-client-paging-rest/karma.conf.js create mode 100644 sdk/core/core-client-paging-rest/package.json create mode 100644 sdk/core/core-client-paging-rest/review/core-client-paging.api.md create mode 100644 sdk/core/core-client-paging-rest/rollup.config.js create mode 100644 sdk/core/core-client-paging-rest/src/index.ts rename sdk/core/{core-client-rest => core-client-paging-rest}/src/paginate.ts (88%) create mode 100644 sdk/core/core-client-paging-rest/src/url.browser.ts create mode 100644 sdk/core/core-client-paging-rest/src/url.ts rename sdk/core/{core-client-rest => core-client-paging-rest}/test/paginate.spec.ts (89%) create mode 100644 sdk/core/core-client-paging-rest/tsconfig.json create mode 100644 sdk/core/core-client-paging-rest/tsdoc.json 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/package.json b/sdk/agrifood/agrifood-farming-rest/package.json index 19dbd2c6f665..892e31e174aa 100644 --- a/sdk/agrifood/agrifood-farming-rest/package.json +++ b/sdk/agrifood/agrifood-farming-rest/package.json @@ -85,6 +85,7 @@ "autoPublish": false, "dependencies": { "@azure/core-auth": "^1.3.0", + "@azure-rest/core-client-paging": "1.0.0-beta.1", "@azure-rest/core-client": "1.0.0-beta.4", "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.1.0", diff --git a/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts index 2580ebd96be3..a8bc6d3b2b9c 100644 --- a/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts +++ b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts @@ -306,15 +306,10 @@ import { WeatherCreateDataDeleteJob202Response, WeatherCreateDataDeleteJobdefaultResponse, } from "./responses"; -import { - getClient, - ClientOptions, - Client, - paginateResponse, - PathUncheckedResponse, -} from "@azure-rest/core-client"; +import { getClient, ClientOptions, Client, PathUncheckedResponse } from "@azure-rest/core-client"; import { TokenCredential } from "@azure/core-auth"; import { PagedAsyncIterableIterator } from "@azure/core-paging"; +import { paginateResponse } from "@azure-rest/core-client-paging"; export interface ApplicationDataListByFarmerId { /** Returns a paginated list of application data resources under a particular farm. */ 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..517b7ddecc97 --- /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 stronly 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..770356f6e5a7 --- /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/src/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..f9c97e9dbe9e --- /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/src/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/master/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.4", + "tslib": "^2.0.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..0a644dffa68c --- /dev/null +++ b/sdk/core/core-client-paging-rest/review/core-client-paging.api.md @@ -0,0 +1,21 @@ +## 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'; + +// @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..e08e7a2b3d71 --- /dev/null +++ b/sdk/core/core-client-paging-rest/src/index.ts @@ -0,0 +1,9 @@ +// 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"; diff --git a/sdk/core/core-client-rest/src/paginate.ts b/sdk/core/core-client-paging-rest/src/paginate.ts similarity index 88% rename from sdk/core/core-client-rest/src/paginate.ts rename to sdk/core/core-client-paging-rest/src/paginate.ts index e5c5d3c78597..e1d389c5d51b 100644 --- a/sdk/core/core-client-rest/src/paginate.ts +++ b/sdk/core/core-client-paging-rest/src/paginate.ts @@ -3,8 +3,7 @@ /// -import { Client, HttpResponse, PathUncheckedResponse } from "./"; -import "@azure/core-paging"; +import { Client, 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"]; @@ -21,8 +20,10 @@ 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; + nextLinkName?: string | null; /** * Indicates the name of the property in which the set of values is found. Default: `value` */ @@ -77,6 +78,13 @@ async function* listPage[]>( 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); 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-rest/test/paginate.spec.ts b/sdk/core/core-client-paging-rest/test/paginate.spec.ts similarity index 89% rename from sdk/core/core-client-rest/test/paginate.spec.ts rename to sdk/core/core-client-paging-rest/test/paginate.spec.ts index 8c32974fa295..28404e7975cc 100644 --- a/sdk/core/core-client-rest/test/paginate.spec.ts +++ b/sdk/core/core-client-paging-rest/test/paginate.spec.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { assert } from "chai"; -import { Client, getClient, PathUncheckedResponse } from "../src"; +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"; @@ -104,6 +104,32 @@ describe("Paginate heleper", () => { 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. 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/README.md b/sdk/core/core-client-rest/README.md index cb11ca79f468..4cdcdbe50460 100644 --- a/sdk/core/core-client-rest/README.md +++ b/sdk/core/core-client-rest/README.md @@ -14,83 +14,6 @@ This package is primarily used in generated code and not meant to be consumed di ## 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 stronly 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. 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 a9d452f43274..4a92c695387b 100644 --- a/sdk/core/core-client-rest/review/core-client.api.md +++ b/sdk/core/core-client-rest/review/core-client.api.md @@ -5,7 +5,6 @@ ```ts import { KeyCredential } from '@azure/core-auth'; -import { PagedAsyncIterableIterator } from '@azure/core-paging'; import { Pipeline } from '@azure/core-rest-pipeline'; import { PipelineOptions } from '@azure/core-rest-pipeline'; import { PipelineRequest } from '@azure/core-rest-pipeline'; @@ -65,15 +64,6 @@ export type HttpResponse = { // @public export function isCertificateCredential(credential: unknown): credential is CertificateCredential; -// @public -export interface PaginateOptions { - itemName?: string; - nextLinkName?: string; -} - -// @public -export function paginateResponse(client: Client, initialResponse: HttpResponse, options?: PaginateOptions): PagedAsyncIterableIterator; - // @public export type PathUncheckedResponse = HttpResponse & { body: any; diff --git a/sdk/core/core-client-rest/src/index.ts b/sdk/core/core-client-rest/src/index.ts index d24866b57ed7..ea2e7c612dcf 100644 --- a/sdk/core/core-client-rest/src/index.ts +++ b/sdk/core/core-client-rest/src/index.ts @@ -8,7 +8,6 @@ export { createDefaultPipeline } from "./clientHelpers"; export { CertificateCredential, isCertificateCredential } from "./certificateCredential"; -export { paginateResponse, PaginateOptions } from "./paginate"; export * from "./common"; export * from "./getClient"; export * from "./pathClientTypes"; 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"] } From f99ba3099797051dc8472f1ba2121a0463598cf5 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 21 Jun 2021 12:05:40 -0700 Subject: [PATCH 07/11] Use REST Error --- sdk/core/core-client-paging-rest/README.md | 2 +- sdk/core/core-client-paging-rest/package.json | 2 +- .../core-client-paging-rest/src/paginate.ts | 17 ++++++----- sdk/core/core-client-rest/api-extractor.json | 4 +-- sdk/core/core-client-rest/package.json | 2 +- .../review/core-client.api.md | 4 +++ sdk/core/core-client-rest/src/index.ts | 1 + sdk/core/core-client-rest/src/restError.ts | 29 +++++++++++++++++++ 8 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 sdk/core/core-client-rest/src/restError.ts diff --git a/sdk/core/core-client-paging-rest/README.md b/sdk/core/core-client-paging-rest/README.md index 517b7ddecc97..9d4e9c57b5e7 100644 --- a/sdk/core/core-client-paging-rest/README.md +++ b/sdk/core/core-client-paging-rest/README.md @@ -46,7 +46,7 @@ type GetArrayType = T extends Array ? TData : never; // 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 stronly typed Iterator based on the response we get as second parameter +// 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 }; } diff --git a/sdk/core/core-client-paging-rest/package.json b/sdk/core/core-client-paging-rest/package.json index f9c97e9dbe9e..6a3952608282 100644 --- a/sdk/core/core-client-paging-rest/package.json +++ b/sdk/core/core-client-paging-rest/package.json @@ -5,7 +5,7 @@ "sdk-type": "core", "main": "dist/index.js", "module": "dist-esm/src/index.js", - "types": "types/src/latest/core-client-paging-rest.d.ts", + "types": "types/latest/core-client-paging-rest.d.ts", "browser": { "./dist-esm/src/url.js": "./dist-esm/src/url.browser.js" }, diff --git a/sdk/core/core-client-paging-rest/src/paginate.ts b/sdk/core/core-client-paging-rest/src/paginate.ts index e1d389c5d51b..9aa5796ddabe 100644 --- a/sdk/core/core-client-paging-rest/src/paginate.ts +++ b/sdk/core/core-client-paging-rest/src/paginate.ts @@ -3,7 +3,12 @@ /// -import { Client, HttpResponse, PathUncheckedResponse } from "@azure-rest/core-client"; +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"]; @@ -99,13 +104,9 @@ async function* listPage[]>( */ function checkPagingRequest(response: PathUncheckedResponse) { if (!Http2xxStatusCodes.includes(response.status)) { - throw ( - response.body?.error ?? { - message: `Pagination failed`, - request: response.request, - response: response, - status: response.status, - } + throw createRestError( + `Pagination failed with unexpected statusCode ${response.status}`, + response ); } } 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/package.json b/sdk/core/core-client-rest/package.json index d9ad55259024..2b61c63dfe0c 100644 --- a/sdk/core/core-client-rest/package.json +++ b/sdk/core/core-client-rest/package.json @@ -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 4a92c695387b..5b3af4a047f7 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 @@ -47,6 +48,9 @@ export type ClientOptions = PipelineOptions & { // @public export function createDefaultPipeline(baseUrl: string, credential?: TokenCredential | KeyCredential, options?: ClientOptions): Pipeline; +// @public +export function createRestError(message: string, response: PathUncheckedResponse): RestError; + // @public export function getClient(baseUrl: string, options?: ClientOptions): Client; 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/restError.ts b/sdk/core/core-client-rest/src/restError.ts new file mode 100644 index 000000000000..8353266918d5 --- /dev/null +++ b/sdk/core/core-client-rest/src/restError.ts @@ -0,0 +1,29 @@ +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) { + 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) { + if (Number.isNaN(statusCode)) { + return undefined; + } + + return Number.parseInt(statusCode); +} From 2e3b5c6b07db34cf7b53085c43f0fadd01573612 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 23 Jun 2021 13:30:23 -0700 Subject: [PATCH 08/11] Update types output --- common/config/rush/pnpm-lock.yaml | 44 ++++++++++++++++++- .../api-extractor.json | 2 +- sdk/core/core-client-paging-rest/package.json | 2 +- 3 files changed, 44 insertions(+), 4 deletions(-) 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/sdk/core/core-client-paging-rest/api-extractor.json b/sdk/core/core-client-paging-rest/api-extractor.json index 770356f6e5a7..5f0bb62e9090 100644 --- a/sdk/core/core-client-paging-rest/api-extractor.json +++ b/sdk/core/core-client-paging-rest/api-extractor.json @@ -11,7 +11,7 @@ "dtsRollup": { "enabled": true, "untrimmedFilePath": "", - "publicTrimmedFilePath": "./types/src/latest/core-client-paging-rest.d.ts" + "publicTrimmedFilePath": "./types/latest/core-client-paging-rest.d.ts" }, "messages": { "tsdocMessageReporting": { diff --git a/sdk/core/core-client-paging-rest/package.json b/sdk/core/core-client-paging-rest/package.json index 6a3952608282..224d9119c57f 100644 --- a/sdk/core/core-client-paging-rest/package.json +++ b/sdk/core/core-client-paging-rest/package.json @@ -64,7 +64,7 @@ "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.0.3", "@azure-rest/core-client": "1.0.0-beta.4", - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "devDependencies": { "@microsoft/api-extractor": "7.13.2", From b49abc57f8c3d19cb9448d89a886ea2643086b1e Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 23 Jun 2021 13:34:55 -0700 Subject: [PATCH 09/11] update home page --- sdk/core/core-client-paging-rest/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/core-client-paging-rest/package.json b/sdk/core/core-client-paging-rest/package.json index 224d9119c57f..987166ad39fc 100644 --- a/sdk/core/core-client-paging-rest/package.json +++ b/sdk/core/core-client-paging-rest/package.json @@ -57,7 +57,7 @@ "engines": { "node": ">=8.0.0" }, - "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-client-paging-rest/", + "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": { From 9e208a2efe159308eec411748900f768ae1ef817 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 23 Jun 2021 15:07:10 -0700 Subject: [PATCH 10/11] Explicit return RestError --- sdk/core/core-client-rest/src/restError.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/core/core-client-rest/src/restError.ts b/sdk/core/core-client-rest/src/restError.ts index 8353266918d5..f12048ea07f1 100644 --- a/sdk/core/core-client-rest/src/restError.ts +++ b/sdk/core/core-client-rest/src/restError.ts @@ -1,10 +1,13 @@ +// 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) { +export function createRestError(message: string, response: PathUncheckedResponse): RestError { return new RestError(message, { statusCode: statusCodeToNumber(response.status), request: response.request, From 463bafa16a6a0982f233bacb3c0b52a16f872a7e Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 23 Jun 2021 22:28:04 -0700 Subject: [PATCH 11/11] Move paging to its own file and update changelog and versions --- .../agrifood-farming-rest/CHANGELOG.md | 3 ++ .../agrifood-farming-rest/package.json | 3 +- .../review/agrifood-farming.api.md | 2 +- .../agrifood-farming-rest/src/farmBeats.ts | 35 +----------------- .../agrifood-farming-rest/src/index.ts | 2 +- .../agrifood-farming-rest/src/paging.ts | 37 +++++++++++++++++++ .../confidential-ledger-rest/package.json | 2 +- sdk/core/core-client-paging-rest/package.json | 2 +- .../review/core-client-paging.api.md | 2 + sdk/core/core-client-paging-rest/src/index.ts | 1 + sdk/core/core-client-rest/CHANGELOG.md | 9 ++++- sdk/core/core-client-rest/package.json | 3 +- .../review/core-client.api.md | 6 ++- .../core-client-rest/src/pathClientTypes.ts | 26 +++++++++---- sdk/core/core-client-rest/src/restError.ts | 12 +++--- .../ai-document-translator-rest/package.json | 2 +- sdk/purview/purview-catalog-rest/package.json | 2 +- .../purview-scanning-rest/package.json | 2 +- 18 files changed, 90 insertions(+), 61 deletions(-) create mode 100644 sdk/agrifood/agrifood-farming-rest/src/paging.ts 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 892e31e174aa..f431a0a52a71 100644 --- a/sdk/agrifood/agrifood-farming-rest/package.json +++ b/sdk/agrifood/agrifood-farming-rest/package.json @@ -86,8 +86,7 @@ "dependencies": { "@azure/core-auth": "^1.3.0", "@azure-rest/core-client-paging": "1.0.0-beta.1", - "@azure-rest/core-client": "1.0.0-beta.4", - "@azure/core-paging": "^1.1.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 02a7a705dacd..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,7 +7,7 @@ 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/core-paging'; +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'; diff --git a/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts index a8bc6d3b2b9c..9e439797a8e2 100644 --- a/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts +++ b/sdk/agrifood/agrifood-farming-rest/src/farmBeats.ts @@ -306,10 +306,8 @@ import { WeatherCreateDataDeleteJob202Response, WeatherCreateDataDeleteJobdefaultResponse, } from "./responses"; -import { getClient, ClientOptions, Client, PathUncheckedResponse } from "@azure-rest/core-client"; +import { getClient, ClientOptions, Client } from "@azure-rest/core-client"; import { TokenCredential } from "@azure/core-auth"; -import { PagedAsyncIterableIterator } from "@azure/core-paging"; -import { paginateResponse } from "@azure-rest/core-client-paging"; export interface ApplicationDataListByFarmerId { /** Returns a paginated list of application data resources under a particular farm. */ @@ -1112,37 +1110,6 @@ export type FarmBeatsRestClient = Client & { path: Routes; }; -/** - * 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); -} export default function FarmBeats( Endpoint: string, credentials: TokenCredential, diff --git a/sdk/agrifood/agrifood-farming-rest/src/index.ts b/sdk/agrifood/agrifood-farming-rest/src/index.ts index a77eb7c82796..8bc909814a2a 100644 --- a/sdk/agrifood/agrifood-farming-rest/src/index.ts +++ b/sdk/agrifood/agrifood-farming-rest/src/index.ts @@ -8,6 +8,6 @@ export * from "./models"; export * from "./parameters"; export * from "./responses"; -export { paginate, PaginateReturn, GetArrayType } from "./farmBeats"; +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/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/core-client-paging-rest/package.json b/sdk/core/core-client-paging-rest/package.json index 987166ad39fc..280db0713bc3 100644 --- a/sdk/core/core-client-paging-rest/package.json +++ b/sdk/core/core-client-paging-rest/package.json @@ -63,7 +63,7 @@ "dependencies": { "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.0.3", - "@azure-rest/core-client": "1.0.0-beta.4", + "@azure-rest/core-client": "1.0.0-beta.5", "tslib": "^2.2.0" }, "devDependencies": { 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 index 0a644dffa68c..e3a6c7305a9c 100644 --- 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 @@ -8,6 +8,8 @@ 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; diff --git a/sdk/core/core-client-paging-rest/src/index.ts b/sdk/core/core-client-paging-rest/src/index.ts index e08e7a2b3d71..902316ffaffd 100644 --- a/sdk/core/core-client-paging-rest/src/index.ts +++ b/sdk/core/core-client-paging-rest/src/index.ts @@ -7,3 +7,4 @@ * @packageDocumentation */ export { paginateResponse, PaginateOptions } from "./paginate"; +export { PagedAsyncIterableIterator } from "@azure/core-paging"; 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/package.json b/sdk/core/core-client-rest/package.json index 2b61c63dfe0c..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", @@ -61,7 +61,6 @@ "sideEffects": false, "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", "dependencies": { - "@azure/core-paging": "^1.1.1", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.1.0", "tslib": "^2.2.0" 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 5b3af4a047f7..a853c4d6ef32 100644 --- a/sdk/core/core-client-rest/review/core-client.api.md +++ b/sdk/core/core-client-rest/review/core-client.api.md @@ -84,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/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 index f12048ea07f1..1cb18be69502 100644 --- a/sdk/core/core-client-rest/src/restError.ts +++ b/sdk/core/core-client-rest/src/restError.ts @@ -11,11 +11,11 @@ export function createRestError(message: string, response: PathUncheckedResponse return new RestError(message, { statusCode: statusCodeToNumber(response.status), request: response.request, - response: toPipelineresponse(response), + response: toPipelineResponse(response), }); } -function toPipelineresponse(response: PathUncheckedResponse): PipelineResponse { +function toPipelineResponse(response: PathUncheckedResponse): PipelineResponse { return { headers: createHttpHeaders(response.headers), request: response.request, @@ -23,10 +23,8 @@ function toPipelineresponse(response: PathUncheckedResponse): PipelineResponse { }; } -function statusCodeToNumber(statusCode: string) { - if (Number.isNaN(statusCode)) { - return undefined; - } +function statusCodeToNumber(statusCode: string): number | undefined { + const status = Number.parseInt(statusCode); - return Number.parseInt(statusCode); + return Number.isNaN(status) ? undefined : status; } 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"