Skip to content

Commit 95c5bd2

Browse files
authored
feat(client)!: Return a friendly type from handle.describe() (#532)
* feat(client)!: Return a friendly type from handle.describe() * Address comments * Fix tests
1 parent f4fad3a commit 95c5bd2

File tree

6 files changed

+159
-57
lines changed

6 files changed

+159
-57
lines changed

packages/client/src/types.ts

+16
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,19 @@ export type DescribeWorkflowExecutionResponse = temporal.api.workflowservice.v1.
1010
export type TerminateWorkflowExecutionResponse = temporal.api.workflowservice.v1.ITerminateWorkflowExecutionResponse;
1111
export type RequestCancelWorkflowExecutionResponse =
1212
temporal.api.workflowservice.v1.IRequestCancelWorkflowExecutionResponse;
13+
14+
export interface WorkflowExecutionDescription {
15+
type: string;
16+
workflowId: string;
17+
runId: string;
18+
taskQueue: string;
19+
status: temporal.api.enums.v1.WorkflowExecutionStatus;
20+
historyLength: Long;
21+
startTime: Date;
22+
executionTime?: Date;
23+
closeTime?: Date;
24+
memo?: Record<string, unknown>;
25+
searchAttributes?: Record<string, unknown>;
26+
parentExecution?: Required<temporal.api.common.v1.IWorkflowExecution>;
27+
raw: DescribeWorkflowExecutionResponse;
28+
}

packages/client/src/workflow-client.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import {
1313
decodeArrayFromPayloads,
1414
decodeFromPayloadsAtIndex,
15+
decodeMapFromPayloads,
1516
decodeOptionalFailureToOptionalError,
1617
encodeMapToPayloads,
1718
encodeToPayloads,
@@ -21,8 +22,10 @@ import {
2122
BaseWorkflowHandle,
2223
compileRetryPolicy,
2324
composeInterceptors,
25+
optionalTsToDate,
2426
QueryDefinition,
2527
SignalDefinition,
28+
tsToDate,
2629
WithWorkflowArgs,
2730
Workflow,
2831
WorkflowNotFoundError,
@@ -57,6 +60,7 @@ import {
5760
StartWorkflowExecutionRequest,
5861
TerminateWorkflowExecutionResponse,
5962
WorkflowExecution,
63+
WorkflowExecutionDescription,
6064
} from './types';
6165
import { compileWorkflowOptions, WorkflowOptions, WorkflowSignalWithStartOptions } from './workflow-options';
6266

@@ -113,7 +117,7 @@ export interface WorkflowHandle<T extends Workflow = Workflow> extends BaseWorkf
113117
/**
114118
* Describe the current workflow execution
115119
*/
116-
describe(): Promise<DescribeWorkflowExecutionResponse>;
120+
describe(): Promise<WorkflowExecutionDescription>;
117121

118122
/**
119123
* Readonly accessor to the underlying WorkflowClient
@@ -765,9 +769,36 @@ export class WorkflowClient {
765769
async describe() {
766770
const next = this.client._describeWorkflowHandler.bind(this.client);
767771
const fn = interceptors.length ? composeInterceptors(interceptors, 'describe', next) : next;
768-
return await fn({
772+
const raw = await fn({
769773
workflowExecution: { workflowId, runId },
770774
});
775+
return {
776+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
777+
type: raw.workflowExecutionInfo!.type!.name!,
778+
workflowId: raw.workflowExecutionInfo!.execution!.workflowId!,
779+
runId: raw.workflowExecutionInfo!.execution!.runId!,
780+
taskQueue: raw.workflowExecutionInfo!.taskQueue!,
781+
status: raw.workflowExecutionInfo!.status!,
782+
historyLength: raw.workflowExecutionInfo!.historyLength!,
783+
startTime: tsToDate(raw.workflowExecutionInfo!.startTime!),
784+
executionTime: optionalTsToDate(raw.workflowExecutionInfo!.executionTime),
785+
closeTime: optionalTsToDate(raw.workflowExecutionInfo!.closeTime),
786+
memo: await decodeMapFromPayloads(
787+
this.client.options.loadedDataConverter,
788+
raw.workflowExecutionInfo!.memo?.fields
789+
),
790+
searchAttributes: await decodeMapFromPayloads(
791+
defaultDataConverter,
792+
raw.workflowExecutionInfo!.searchAttributes?.indexedFields
793+
),
794+
parentExecution: raw.workflowExecutionInfo!.parentExecution
795+
? {
796+
workflowId: raw.workflowExecutionInfo!.parentExecution!.workflowId!,
797+
runId: raw.workflowExecutionInfo!.parentExecution!.runId!,
798+
}
799+
: undefined,
800+
raw,
801+
};
771802
},
772803
async signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
773804
const next = this.client._signalWorkflowHandler.bind(this.client);

packages/internal-non-workflow-common/src/codec-helpers.ts

+41-12
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
toPayload,
1313
toPayloads,
1414
} from '@temporalio/common';
15-
1615
import { DecodedPayload, DecodedProtoFailure, EncodedPayload, EncodedProtoFailure } from './codec-types';
1716

1817
export interface TypecheckedPayloadCodec {
@@ -63,7 +62,8 @@ export async function encodeOptional(
6362
codec: PayloadCodec,
6463
payloads: Payload[] | null | undefined
6564
): Promise<EncodedPayload[] | null | undefined> {
66-
if (!payloads) return payloads;
65+
if (payloads === null) return null;
66+
if (payloads === undefined) return undefined;
6767
return (await codec.encode(payloads)) as EncodedPayload[];
6868
}
6969

@@ -72,7 +72,8 @@ export async function decodeOptional(
7272
codec: PayloadCodec,
7373
payloads: Payload[] | null | undefined
7474
): Promise<DecodedPayload[] | null | undefined> {
75-
if (!payloads) return payloads;
75+
if (payloads === null) return null;
76+
if (payloads === undefined) return undefined;
7677
return (await codec.decode(payloads)) as DecodedPayload[];
7778
}
7879

@@ -86,7 +87,8 @@ export async function encodeOptionalSingle(
8687
codec: PayloadCodec,
8788
payload: Payload | null | undefined
8889
): Promise<EncodedPayload | null | undefined> {
89-
if (!payload) return payload;
90+
if (payload === null) return null;
91+
if (payload === undefined) return undefined;
9092
return await encodeSingle(codec, payload);
9193
}
9294

@@ -100,7 +102,9 @@ export async function decodeOptionalSingle(
100102
codec: PayloadCodec,
101103
payload: Payload | null | undefined
102104
): Promise<DecodedPayload | null | undefined> {
103-
if (!payload) return payload;
105+
if (payload === null) return null;
106+
if (payload === undefined) return undefined;
107+
104108
return await decodeSingle(codec, payload);
105109
}
106110

@@ -127,12 +131,33 @@ export async function encodeToPayloads(
127131
return payloads ? await payloadCodec.encode(payloads) : undefined;
128132
}
129133

134+
/**
135+
* Run {@link PayloadCodec.decode} and then {@link PayloadConverter.fromPayload} on values in `map`.
136+
*/
137+
export async function decodeMapFromPayloads<K extends string>(
138+
converter: LoadedDataConverter,
139+
map: Record<K, Payload> | null | undefined
140+
): Promise<Record<K, unknown> | undefined> {
141+
if (!map) return undefined;
142+
const { payloadConverter, payloadCodec } = converter;
143+
return Object.fromEntries(
144+
await Promise.all(
145+
Object.entries(map).map(async ([k, payload]): Promise<[K, unknown]> => {
146+
const [decodedPayload] = await payloadCodec.decode([payload as Payload]);
147+
const value = payloadConverter.fromPayload(decodedPayload);
148+
return [k as K, value];
149+
})
150+
)
151+
) as Record<K, unknown>;
152+
}
153+
130154
/** Run {@link PayloadCodec.encode} on all values in `map` */
131155
export async function encodeMap<K extends string>(
132156
codec: PayloadCodec,
133157
map: Record<K, Payload> | null | undefined
134158
): Promise<Record<K, EncodedPayload> | null | undefined> {
135-
if (!map) return map;
159+
if (map === null) return null;
160+
if (map === undefined) return undefined;
136161
return Object.fromEntries(
137162
await Promise.all(
138163
Object.entries(map).map(async ([k, payload]): Promise<[K, EncodedPayload]> => {
@@ -143,11 +168,11 @@ export async function encodeMap<K extends string>(
143168
}
144169

145170
/**
146-
* Run {@link PayloadConverter.toPayload} and {@link PayloadCodec.encode} on values in `map`.
171+
* Run {@link PayloadConverter.toPayload} and then {@link PayloadCodec.encode} on values in `map`.
147172
*/
148173
export async function encodeMapToPayloads<K extends string>(
149174
converter: LoadedDataConverter,
150-
map: Record<K, any>
175+
map: Record<K, unknown>
151176
): Promise<Record<K, Payload>> {
152177
const { payloadConverter, payloadCodec } = converter;
153178
return Object.fromEntries(
@@ -228,7 +253,8 @@ export async function encodeOptionalFailure(
228253
codec: PayloadCodec,
229254
failure: ProtoFailure | null | undefined
230255
): Promise<EncodedProtoFailure | null | undefined> {
231-
if (!failure) return failure;
256+
if (failure === null) return null;
257+
if (failure === undefined) return undefined;
232258
return await encodeFailure(codec, failure);
233259
}
234260

@@ -239,7 +265,8 @@ export async function decodeOptionalFailure(
239265
codec: PayloadCodec,
240266
failure: ProtoFailure | null | undefined
241267
): Promise<DecodedProtoFailure | null | undefined> {
242-
if (!failure) return failure;
268+
if (failure === null) return null;
269+
if (failure === undefined) return undefined;
243270
return await decodeFailure(codec, failure);
244271
}
245272

@@ -301,7 +328,8 @@ export async function decodeFailure(_codec: PayloadCodec, failure: ProtoFailure)
301328
export function noopEncodeMap<K extends string>(
302329
map: Record<K, Payload> | null | undefined
303330
): Record<K, EncodedPayload> | null | undefined {
304-
if (!map) return map;
331+
if (map === null) return null;
332+
if (map === undefined) return undefined;
305333
return map as Record<K, EncodedPayload>;
306334
}
307335

@@ -312,6 +340,7 @@ export function noopEncodeMap<K extends string>(
312340
export function noopDecodeMap<K extends string>(
313341
map: Record<K, Payload> | null | undefined
314342
): Record<K, DecodedPayload> | null | undefined {
315-
if (!map) return map;
343+
if (map === null) return null;
344+
if (map === undefined) return undefined;
316345
return map as Record<K, DecodedPayload>;
317346
}

packages/internal-workflow-common/src/time.ts

+11
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,14 @@ export function msToNumber(val: string | number): number {
7474
}
7575
return ms(val);
7676
}
77+
78+
export function tsToDate(ts: Timestamp): Date {
79+
return new Date(tsToMs(ts));
80+
}
81+
82+
export function optionalTsToDate(ts: Timestamp | null | undefined): Date | undefined {
83+
if (ts === undefined || ts === null) {
84+
return undefined;
85+
}
86+
return new Date(tsToMs(ts));
87+
}

packages/test/src/integration-tests.ts

+50-31
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,28 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
538538
t.regex(event.workflowTaskCompletedEventAttributes!.binaryChecksum!, /@temporalio\/worker@\d+\.\d+\.\d+/);
539539
});
540540

541+
test('WorkflowHandle.describe result is wrapped', async (t) => {
542+
const { client } = t.context;
543+
const workflow = await client.start(workflows.argsAndReturn, {
544+
args: ['hey', undefined, Buffer.from('def')],
545+
taskQueue: 'test',
546+
workflowId: uuid4(),
547+
searchAttributes: {
548+
CustomKeywordField: 'test-value',
549+
},
550+
memo: {
551+
note: 'foo',
552+
},
553+
});
554+
await workflow.result();
555+
const execution = await workflow.describe();
556+
t.deepEqual(execution.type, 'argsAndReturn');
557+
t.deepEqual(execution.memo, { note: 'foo' });
558+
t.true(execution.startTime instanceof Date);
559+
t.is(execution.searchAttributes!.CustomKeywordField, 'test-value');
560+
t.regex((execution.searchAttributes!.BinaryChecksums as string[])[0], /@temporalio\/worker@/);
561+
});
562+
541563
test('WorkflowOptions are passed correctly with defaults', async (t) => {
542564
const { client } = t.context;
543565
const workflow = await client.start(workflows.argsAndReturn, {
@@ -547,22 +569,23 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
547569
});
548570
await workflow.result();
549571
const execution = await workflow.describe();
550-
t.deepEqual(
551-
execution.workflowExecutionInfo?.type,
552-
new iface.temporal.api.common.v1.WorkflowType({ name: 'argsAndReturn' })
553-
);
554-
t.deepEqual(execution.workflowExecutionInfo?.memo, new iface.temporal.api.common.v1.Memo({ fields: {} }));
555-
t.deepEqual(Object.keys(execution.workflowExecutionInfo!.searchAttributes!.indexedFields!), ['BinaryChecksums']);
572+
t.deepEqual(execution.type, 'argsAndReturn');
573+
t.deepEqual(Object.keys(execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!), [
574+
'BinaryChecksums',
575+
]);
556576

557577
const checksums = defaultPayloadConverter.fromPayload(
558-
execution.workflowExecutionInfo!.searchAttributes!.indexedFields!.BinaryChecksums!
578+
execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!.BinaryChecksums!
559579
);
560580
t.true(checksums instanceof Array && checksums.length === 1);
561581
t.regex((checksums as string[])[0], /@temporalio\/worker@\d+\.\d+\.\d+/);
562-
t.is(execution.executionConfig?.taskQueue?.name, 'test');
563-
t.is(execution.executionConfig?.taskQueue?.kind, iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL);
564-
t.is(execution.executionConfig?.workflowRunTimeout, null);
565-
t.is(execution.executionConfig?.workflowExecutionTimeout, null);
582+
t.is(execution.raw.executionConfig?.taskQueue?.name, 'test');
583+
t.is(
584+
execution.raw.executionConfig?.taskQueue?.kind,
585+
iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL
586+
);
587+
t.is(execution.raw.executionConfig?.workflowRunTimeout, null);
588+
t.is(execution.raw.executionConfig?.workflowExecutionTimeout, null);
566589
});
567590

568591
test('WorkflowOptions are passed correctly', async (t) => {
@@ -584,22 +607,25 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
584607
});
585608
const execution = await workflow.describe();
586609
t.deepEqual(
587-
execution.workflowExecutionInfo?.type,
610+
execution.raw.workflowExecutionInfo?.type,
588611
new iface.temporal.api.common.v1.WorkflowType({ name: 'sleeper' })
589612
);
590-
t.deepEqual(await fromPayload(execution.workflowExecutionInfo!.memo!.fields!.a!), 'b');
613+
t.deepEqual(await fromPayload(execution.raw.workflowExecutionInfo!.memo!.fields!.a!), 'b');
591614
t.deepEqual(
592615
await defaultPayloadConverter.fromPayload(
593-
execution.workflowExecutionInfo!.searchAttributes!.indexedFields!.CustomIntField!
616+
execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!.CustomIntField!
594617
),
595618
3
596619
);
597-
t.is(execution.executionConfig?.taskQueue?.name, 'test2');
598-
t.is(execution.executionConfig?.taskQueue?.kind, iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL);
620+
t.is(execution.raw.executionConfig?.taskQueue?.name, 'test2');
621+
t.is(
622+
execution.raw.executionConfig?.taskQueue?.kind,
623+
iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL
624+
);
599625

600-
t.is(tsToMs(execution.executionConfig!.workflowRunTimeout!), ms(options.workflowRunTimeout));
601-
t.is(tsToMs(execution.executionConfig!.workflowExecutionTimeout!), ms(options.workflowExecutionTimeout));
602-
t.is(tsToMs(execution.executionConfig!.defaultWorkflowTaskTimeout!), ms(options.workflowTaskTimeout));
626+
t.is(tsToMs(execution.raw.executionConfig!.workflowRunTimeout!), ms(options.workflowRunTimeout));
627+
t.is(tsToMs(execution.raw.executionConfig!.workflowExecutionTimeout!), ms(options.workflowExecutionTimeout));
628+
t.is(tsToMs(execution.raw.executionConfig!.defaultWorkflowTaskTimeout!), ms(options.workflowTaskTimeout));
603629
});
604630

605631
test('WorkflowHandle.result() throws if terminated', async (t) => {
@@ -667,7 +693,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
667693
});
668694
await workflow.result();
669695
const info = await workflow.describe();
670-
t.is(info.workflowExecutionInfo?.type?.name, 'sleeper');
696+
t.is(info.raw.workflowExecutionInfo?.type?.name, 'sleeper');
671697
const { history } = await client.service.getWorkflowExecutionHistory({
672698
namespace,
673699
execution: { workflowId: workflow.workflowId, runId: err.newExecutionRunId },
@@ -753,14 +779,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
753779
return;
754780
}
755781
t.is(failure.message, 'unhandled rejection');
756-
t.true(
757-
failure.stackTrace?.includes(
758-
dedent`
759-
Error: unhandled rejection
760-
at eval (webpack-internal:///./lib/workflows/unhandled-rejection.js
761-
`
762-
)
763-
);
782+
t.true(failure.stackTrace?.includes(`Error: unhandled rejection`));
764783
t.is(failure.cause?.message, 'root failure');
765784
},
766785
{ minTimeout: 300, factor: 1, retries: 100 }
@@ -815,8 +834,8 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
815834
});
816835
await t.throwsAsync(handle.result());
817836
const handleForSecondAttempt = client.getHandle(workflowId);
818-
const { workflowExecutionInfo } = await handleForSecondAttempt.describe();
819-
t.not(workflowExecutionInfo?.execution?.runId, handle.originalRunId);
837+
const { raw } = await handleForSecondAttempt.describe();
838+
t.not(raw.workflowExecutionInfo?.execution?.runId, handle.originalRunId);
820839
});
821840

822841
test('Workflow RetryPolicy ignored with nonRetryable failure', async (t) => {
@@ -835,7 +854,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
835854
await t.throwsAsync(handle.result());
836855
const res = await handle.describe();
837856
t.is(
838-
res.workflowExecutionInfo?.status,
857+
res.raw.workflowExecutionInfo?.status,
839858
iface.temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED
840859
);
841860
});

0 commit comments

Comments
 (0)