Skip to content

Commit 53233c9

Browse files
authored
feat(client): add client.workflow.count high level API (#1573)
1 parent de09f6c commit 53233c9

File tree

4 files changed

+98
-6
lines changed

4 files changed

+98
-6
lines changed

packages/client/src/helpers.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import { Replace } from '@temporalio/common/lib/type-helpers';
1010
import { optionalTsToDate, requiredTsToDate } from '@temporalio/common/lib/time';
1111
import { decodeMapFromPayloads } from '@temporalio/common/lib/internal-non-workflow/codec-helpers';
1212
import { temporal, google } from '@temporalio/proto';
13-
import { RawWorkflowExecutionInfo, WorkflowExecutionInfo, WorkflowExecutionStatusName } from './types';
13+
import {
14+
CountWorkflowExecution,
15+
RawWorkflowExecutionInfo,
16+
WorkflowExecutionInfo,
17+
WorkflowExecutionStatusName,
18+
} from './types';
1419

1520
function workflowStatusCodeToName(code: temporal.api.enums.v1.WorkflowExecutionStatus): WorkflowExecutionStatusName {
1621
return workflowStatusCodeToNameInternal(code) ?? 'UNKNOWN';
@@ -81,6 +86,22 @@ export async function executionInfoFromRaw<T>(
8186
};
8287
}
8388

89+
export function decodeCountWorkflowExecutionsResponse(
90+
raw: temporal.api.workflowservice.v1.ICountWorkflowExecutionsResponse
91+
): CountWorkflowExecution {
92+
return {
93+
// Note: lossy conversion of Long to number
94+
count: raw.count!.toNumber(),
95+
groups: raw.groups!.map((group) => {
96+
return {
97+
// Note: lossy conversion of Long to number
98+
count: group.count!.toNumber(),
99+
groupValues: group.groupValues!.map((value) => searchAttributePayloadConverter.fromPayload(value)),
100+
};
101+
}),
102+
};
103+
}
104+
84105
type ErrorDetailsName = `temporal.api.errordetails.v1.${keyof typeof temporal.api.errordetails.v1}`;
85106

86107
/**

packages/client/src/types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type * as grpc from '@grpc/grpc-js';
2-
import type { SearchAttributes } from '@temporalio/common';
2+
import type { SearchAttributes, SearchAttributeValue } from '@temporalio/common';
33
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
44
import * as proto from '@temporalio/proto';
55
import { Replace } from '@temporalio/common/lib/type-helpers';
@@ -52,6 +52,14 @@ export interface WorkflowExecutionInfo {
5252
raw: RawWorkflowExecutionInfo;
5353
}
5454

55+
export interface CountWorkflowExecution {
56+
count: number;
57+
groups: {
58+
count: number;
59+
groupValues: SearchAttributeValue[];
60+
}[];
61+
}
62+
5563
export type WorkflowExecutionDescription = Replace<
5664
WorkflowExecutionInfo,
5765
{

packages/client/src/workflow-client.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
WorkflowStartUpdateOutput,
5959
} from './interceptors';
6060
import {
61+
CountWorkflowExecution,
6162
DescribeWorkflowExecutionResponse,
6263
encodeQueryRejectCondition,
6364
GetWorkflowExecutionHistoryRequest,
@@ -77,7 +78,7 @@ import {
7778
WorkflowStartOptions,
7879
WorkflowUpdateOptions,
7980
} from './workflow-options';
80-
import { executionInfoFromRaw, rethrowKnownErrorTypes } from './helpers';
81+
import { decodeCountWorkflowExecutionsResponse, executionInfoFromRaw, rethrowKnownErrorTypes } from './helpers';
8182
import {
8283
BaseClient,
8384
BaseClientOptions,
@@ -1285,9 +1286,9 @@ export class WorkflowClient extends BaseClient {
12851286
}
12861287

12871288
/**
1288-
* List workflows by given `query`.
1289+
* Return a list of Workflow Executions matching the given `query`.
12891290
*
1290-
* ⚠️ To use advanced query functionality, as of the 1.18 server release, you must use Elasticsearch based visibility.
1291+
* Note that the list of Workflow Executions returned is approximate and eventually consistent.
12911292
*
12921293
* More info on the concept of "visibility" and the query syntax on the Temporal documentation site:
12931294
* https://docs.temporal.io/visibility
@@ -1308,6 +1309,29 @@ export class WorkflowClient extends BaseClient {
13081309
};
13091310
}
13101311

1312+
/**
1313+
* Return the number of Workflow Executions matching the given `query`. If no `query` is provided, then return the
1314+
* total number of Workflow Executions for this namespace.
1315+
*
1316+
* Note that the number of Workflow Executions returned is approximate and eventually consistent.
1317+
*
1318+
* More info on the concept of "visibility" and the query syntax on the Temporal documentation site:
1319+
* https://docs.temporal.io/visibility
1320+
*/
1321+
public async count(query?: string): Promise<CountWorkflowExecution> {
1322+
let response: temporal.api.workflowservice.v1.CountWorkflowExecutionsResponse;
1323+
try {
1324+
response = await this.workflowService.countWorkflowExecutions({
1325+
namespace: this.options.namespace,
1326+
query,
1327+
});
1328+
} catch (e) {
1329+
this.rethrowGrpcError(e, 'Failed to count workflows');
1330+
}
1331+
1332+
return decodeCountWorkflowExecutionsResponse(response);
1333+
}
1334+
13111335
protected getOrMakeInterceptors(workflowId: string, runId?: string): WorkflowClientInterceptor[] {
13121336
if (typeof this.options.interceptors === 'object' && 'calls' in this.options.interceptors) {
13131337
// eslint-disable-next-line deprecation/deprecation

packages/test/src/test-integration-workflows.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { randomUUID } from 'crypto';
22
import { ExecutionContext } from 'ava';
33
import { firstValueFrom, Subject } from 'rxjs';
4-
import { WorkflowFailedError } from '@temporalio/client';
4+
import { CountWorkflowExecution, WorkflowFailedError } from '@temporalio/client';
55
import * as activity from '@temporalio/activity';
66
import { msToNumber, tsToMs } from '@temporalio/common/lib/time';
77
import { TestWorkflowEnvironment } from '@temporalio/testing';
@@ -1264,3 +1264,42 @@ export const interceptors: workflow.WorkflowInterceptorsFactory = () => {
12641264
}
12651265
return {};
12661266
};
1267+
1268+
export async function completableWorkflow(completes: boolean): Promise<void> {
1269+
await workflow.condition(() => completes);
1270+
}
1271+
1272+
test('Count workflow executions', async (t) => {
1273+
const { taskQueue, createWorker, executeWorkflow, startWorkflow } = helpers(t);
1274+
const worker = await createWorker();
1275+
const client = t.context.env.client;
1276+
1277+
// Run 2 workflows that don't complete
1278+
// (use startWorkflow to avoid waiting for workflows to complete, which they never will)
1279+
for (let i = 0; i < 2; i++) {
1280+
await startWorkflow(completableWorkflow, { args: [false] });
1281+
}
1282+
1283+
await worker.runUntil(async () => {
1284+
// Run 3 workflows that complete.
1285+
await Promise.all([
1286+
executeWorkflow(completableWorkflow, { args: [true] }),
1287+
executeWorkflow(completableWorkflow, { args: [true] }),
1288+
executeWorkflow(completableWorkflow, { args: [true] }),
1289+
]);
1290+
});
1291+
1292+
const actualTotal = await client.workflow.count(`TaskQueue = '${taskQueue}'`);
1293+
t.deepEqual(actualTotal, { count: 5, groups: [] });
1294+
1295+
const expectedByExecutionStatus: CountWorkflowExecution = {
1296+
count: 5,
1297+
groups: [
1298+
{ count: 2, groupValues: [['Running']] },
1299+
{ count: 3, groupValues: [['Completed']] },
1300+
],
1301+
};
1302+
1303+
const actualByExecutionStatus = await client.workflow.count(`TaskQueue = '${taskQueue}' GROUP BY ExecutionStatus`);
1304+
t.deepEqual(actualByExecutionStatus, expectedByExecutionStatus);
1305+
});

0 commit comments

Comments
 (0)