Repository: Azure/azure-functions-durable-js
Pull request #244 (2021-02-17 01:03:57)

Handle pagination in GetStatus API
Addresses: https://github.com/Azure/azure-functions-durable-js/issues/236

**Context:**
When the `getStatus` / `getStatusBy` APIs are called, their response sizes are capped at 100 results. If more data can be fetched, a continuation token is passed-in in the response header.  Then, to fetch that data, we need to perform another request with that continuation token in the request's header.  This is from: https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-http-api#response-2

**The problem:**
Previously, the Durable Functions for JavaScript `getStatusBy` API did not handle the case where more than 100 results were available.  So it forced users with many results to manually construct the HTTP request by themselves and to manage the continuation token that explicitly. 

**This PR**
This PR augments the `getStatus` / `getStatusBy` APIs to manage continuation tokens automatically, therefore free'ing users from the burden of managing HTTP pagination themselves.

**Follow-up tasks**
To track this item in the Durable Python SDK


Patch:
diff --git a/src/durableorchestrationclient.ts b/src/durableorchestrationclient.ts
index 50b7067..ac62699 100644
--- a/src/durableorchestrationclient.ts
+++ b/src/durableorchestrationclient.ts
@@ -870,7 +870,23 @@ export class DurableOrchestrationClient {
         return origins;
     }
 
-    private async getStatusInternal(options: GetStatusOptions): Promise<AxiosResponse> {
+    /**
+     * Internal method that gets the status of all orchestration instances that
+     * match the specified conditions. It handles pagination automatically by
+     * calling itself recursively until no more continuation tokens are found.
+     * @param options Return orchestration instances which were created
+     *  after this Date.
+     * @param [continuationToken] Continuation token corresponding to the
+     * `x-ms-continuation-token` header, for getting the next batch
+     * of results. Used for recursion.
+     * @param [prevData] Results of a previous request, used internally
+     * to aggregate results during recursion.
+     */
+    private async getStatusInternal(
+        options: GetStatusOptions,
+        continuationToken?: string,
+        prevData?: unknown[]
+    ): Promise<AxiosResponse> {
         let requestUrl: string;
         if (this.clientData.rpcBaseUrl) {
             // Fast local RPC path
@@ -958,7 +974,34 @@ export class DurableOrchestrationClient {
             }
         }
 
-        return this.axiosInstance.get(requestUrl);
+        // If a continuation token is provided, we add it to the request's header
+        let axiosConfig = undefined;
+        if (continuationToken) {
+            axiosConfig = {
+                headers: {
+                    "x-ms-continuation-token": continuationToken,
+                },
+            };
+        }
+
+        // We call the getStatus endpoint and construct a promise callback to handle the recursion
+        // This assumes that, so long as continuation tokens are found, that the http response status
+        // can be safely ignored (either 200 or 202).
+        const response = this.axiosInstance.get(requestUrl, axiosConfig).then((httpResponse) => {
+            // Aggregate results so far
+            const headers = httpResponse.headers;
+            if (prevData) {
+                httpResponse.data = prevData.concat(httpResponse.data);
+            }
+            // If a new continuation token is found, recurse. Otherwise, return the results
+            const token = headers["x-ms-continuation-token"];
+            if (token) {
+                return this.getStatusInternal(options, token, httpResponse.data);
+            }
+            return httpResponse;
+        });
+
+        return response;
     }
 
     private createGenericError(response: AxiosResponse<any>): Error {


Test patch:
diff --git a/test/unit/durableclient-spec.ts b/test/unit/durableclient-spec.ts
index 2d6d7a0..53e3fa8 100644
--- a/test/unit/durableclient-spec.ts
+++ b/test/unit/durableclient-spec.ts
@@ -5,6 +5,7 @@ import nock = require("nock");
 import url = require("url");
 import {
     DurableOrchestrationClient,
+    DurableOrchestrationStatus,
     EntityId,
     EntityStateResponse,
     HttpManagementPayload,
@@ -244,6 +245,64 @@ describe("Durable client RPC endpoint", () => {
             expect(scope.isDone()).to.be.equal(true);
             expect(result).to.be.an("array");
         });
+
+        it("uses continuation header", async () => {
+            const input = JSON.parse(durableClientBindingInputJson) as OrchestrationClientInputData;
+            const client = new DurableOrchestrationClient(input);
+
+            // The getStatusBy() method should do a GET to http://127.0.0.1:17071/durabletask/instances/?createdTimeFrom=2020-01-01T00:00:00Z&createdTimeTo=2020-01-01T23:59:59Z&runtimeStatus=Pending,Running,Completed,Terminated,Failed
+            const expectedUrl = new URL(`${testRpcOrigin}/durabletask/instances/`);
+            const createdTimeFrom = "2020-01-01T00:00:00.000Z";
+            const createdTimeTo = "2020-01-01T23:59:59.000Z";
+            const runtimeStatus = "Pending,Running,Completed,Terminated,Failed";
+
+            // create dummy orchestration status for response
+            const dummyDate = new Date();
+            const dummyStatus = new DurableOrchestrationStatus(
+                "dummyOrchestrationStatus",
+                "123456",
+                dummyDate,
+                dummyDate,
+                "myInput",
+                "myOutput",
+                OrchestrationRuntimeStatus.Completed
+            );
+            const dummyStatusJSON = JSON.stringify(dummyStatus);
+
+            const scopeWithTokenResponse = nock(expectedUrl.origin)
+                .get(expectedUrl.pathname)
+                .query({ createdTimeFrom, createdTimeTo, runtimeStatus })
+                .reply(200, [dummyStatusJSON, dummyStatusJSON], {
+                    "x-ms-continuation-token": "myToken",
+                });
+
+            const scopeNoTokenResponse = nock(expectedUrl.origin, {
+                reqheaders: {
+                    "x-ms-continuation-token": "myToken",
+                },
+            })
+                .get(expectedUrl.pathname)
+                .query({ createdTimeFrom, createdTimeTo, runtimeStatus })
+                .reply(200, [dummyStatusJSON]);
+
+            const statusList = runtimeStatus
+                .split(",")
+                .map(
+                    (status) =>
+                        OrchestrationRuntimeStatus[
+                            status as keyof typeof OrchestrationRuntimeStatus
+                        ]
+                );
+            const result = await client.getStatusBy(
+                new Date(createdTimeFrom),
+                new Date(createdTimeTo),
+                statusList
+            );
+            expect(scopeWithTokenResponse.isDone()).to.be.equal(true);
+            expect(scopeNoTokenResponse.isDone()).to.be.equal(true);
+            expect(result).to.be.an("array");
+            expect(result).to.be.of.length(3);
+        });
     });
 
     describe("terminate()", () => {