Repository: Azure/azure-functions-durable-js
Pull request #247 (2021-02-19 23:24:58)

Promote dev to main for 1.4.6 release
As in title, routine PR.


Patch:
diff --git a/package-lock.json b/package-lock.json
index 893f7e5..a4c107f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "durable-functions",
-  "version": "1.4.4",
+  "version": "1.4.6",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index f97b8eb..636fa3e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "durable-functions",
-    "version": "1.4.5",
+    "version": "1.4.6",
     "description": "Durable Functions library for Node.js Azure Functions",
     "license": "MIT",
     "repository": {
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()", () => {