Skip to content

Commit 60a1e08

Browse files
committed
move user metadata encoding/decoding to user-metadata.ts, add deepMerge to merge/override nested fields in objects (used to override activity options with activities' executeWithOptions), add metadata to child workflows and condition calls
1 parent c491683 commit 60a1e08

File tree

10 files changed

+302
-132
lines changed

10 files changed

+302
-132
lines changed

packages/client/src/schedule-helpers.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
decompileRetryPolicy,
77
extractWorkflowType,
88
LoadedDataConverter,
9+
encodeUserMetadata,
10+
decodeUserMetadata,
911
} from '@temporalio/common';
1012
import {
1113
encodeUnifiedSearchAttributes,
@@ -16,9 +18,7 @@ import { Headers } from '@temporalio/common/lib/interceptors';
1618
import {
1719
decodeArrayFromPayloads,
1820
decodeMapFromPayloads,
19-
decodeOptionalSinglePayload,
2021
encodeMapToPayloads,
21-
encodeOptionalToPayload,
2222
encodeToPayloads,
2323
} from '@temporalio/common/lib/internal-non-workflow';
2424
import { temporal } from '@temporalio/proto';
@@ -271,10 +271,7 @@ export async function encodeScheduleAction(
271271
}
272272
: undefined,
273273
header: { fields: headers },
274-
userMetadata: {
275-
summary: await encodeOptionalToPayload(dataConverter, action?.staticSummary),
276-
details: await encodeOptionalToPayload(dataConverter, action?.staticDetails),
277-
},
274+
userMetadata: await encodeUserMetadata(dataConverter, action.staticSummary, action.staticDetails),
278275
priority: action.priority ? compilePriority(action.priority) : undefined,
279276
},
280277
};
@@ -325,7 +322,7 @@ export async function decodeScheduleAction(
325322
pb: temporal.api.schedule.v1.IScheduleAction
326323
): Promise<ScheduleDescriptionAction> {
327324
if (pb.startWorkflow) {
328-
const userMetadata = pb.startWorkflow?.userMetadata;
325+
const { staticSummary, staticDetails } = await decodeUserMetadata(dataConverter, pb.startWorkflow?.userMetadata);
329326
return {
330327
type: 'startWorkflow',
331328
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -342,8 +339,8 @@ export async function decodeScheduleAction(
342339
workflowExecutionTimeout: optionalTsToMs(pb.startWorkflow.workflowExecutionTimeout),
343340
workflowRunTimeout: optionalTsToMs(pb.startWorkflow.workflowRunTimeout),
344341
workflowTaskTimeout: optionalTsToMs(pb.startWorkflow.workflowTaskTimeout),
345-
staticSummary: (await decodeOptionalSinglePayload(dataConverter, userMetadata?.summary)) ?? undefined,
346-
staticDetails: (await decodeOptionalSinglePayload(dataConverter, userMetadata?.details)) ?? undefined,
342+
staticSummary,
343+
staticDetails,
347344
priority: decodePriority(pb.startWorkflow.priority),
348345
};
349346
}

packages/client/src/workflow-client.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
encodeWorkflowIdConflictPolicy,
2424
WorkflowIdConflictPolicy,
2525
compilePriority,
26+
encodeUserMetadata,
2627
} from '@temporalio/common';
2728
import { encodeUnifiedSearchAttributes } from '@temporalio/common/lib/converter/payload-search-attributes';
2829
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
@@ -34,7 +35,6 @@ import {
3435
decodeOptionalFailureToOptionalError,
3536
decodeOptionalSinglePayload,
3637
encodeMapToPayloads,
37-
encodeOptionalToPayload,
3838
encodeToPayloads,
3939
} from '@temporalio/common/lib/internal-non-workflow';
4040
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow';
@@ -1228,10 +1228,7 @@ export class WorkflowClient extends BaseClient {
12281228
: undefined,
12291229
cronSchedule: options.cronSchedule,
12301230
header: { fields: headers },
1231-
userMetadata: {
1232-
summary: await encodeOptionalToPayload(this.dataConverter, options?.staticSummary),
1233-
details: await encodeOptionalToPayload(this.dataConverter, options?.staticDetails),
1234-
},
1231+
userMetadata: await encodeUserMetadata(this.dataConverter, options.staticSummary, options.staticDetails),
12351232
priority: options.priority ? compilePriority(options.priority) : undefined,
12361233
versioningOverride: options.versioningOverride ?? undefined,
12371234
};
@@ -1301,10 +1298,7 @@ export class WorkflowClient extends BaseClient {
13011298
: undefined,
13021299
cronSchedule: opts.cronSchedule,
13031300
header: { fields: headers },
1304-
userMetadata: {
1305-
summary: await encodeOptionalToPayload(this.dataConverter, opts?.staticSummary),
1306-
details: await encodeOptionalToPayload(this.dataConverter, opts?.staticDetails),
1307-
},
1301+
userMetadata: await encodeUserMetadata(this.dataConverter, opts.staticSummary, opts.staticDetails),
13081302
priority: opts.priority ? compilePriority(opts.priority) : undefined,
13091303
versioningOverride: opts.versioningOverride ?? undefined,
13101304
};

packages/common/src/activity-options.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,12 @@ export interface ActivityOptions {
125125
versioningIntent?: VersioningIntent;
126126

127127
/**
128-
* A single-line fixed summary for this workflow execution that may appear in the UI/CLI.
128+
* A fixed, single-line fixed summary for this workflow execution that may appear in the UI/CLI.
129129
* This can be in single-line Temporal markdown format.
130130
*
131131
* @experimental User metadata is a new API and suspectible to change.
132132
*/
133-
staticSummary?: string;
133+
summary?: string;
134134

135135
/**
136136
* Priority of this activity
@@ -202,10 +202,10 @@ export interface LocalActivityOptions {
202202
cancellationType?: ActivityCancellationType;
203203

204204
/**
205-
* A single-line fixed summary for this workflow execution that may appear in the UI/CLI.
205+
* A fixed, single-line fixed summary for this workflow execution that may appear in the UI/CLI.
206206
* This can be in single-line Temporal markdown format.
207207
*
208208
* @experimental User metadata is a new API and suspectible to change.
209209
*/
210-
staticSummary?: string;
210+
summary?: string;
211211
}

packages/common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export {
3737
TypedSearchAttributes,
3838
defineSearchAttributeKey,
3939
} from './search-attributes';
40+
export * from './user-metadata';
4041

4142
/**
4243
* Encode a UTF-8 string into a Uint8Array

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Payload } from '../interfaces';
2-
import { arrayFromPayloads, fromPayloadsAtIndex, toPayloads } from '../converter/payload-converter';
2+
import { arrayFromPayloads, fromPayloadsAtIndex, PayloadConverter, toPayloads } from '../converter/payload-converter';
33
import { PayloadConverterError } from '../errors';
44
import { PayloadCodec } from '../converter/payload-codec';
55
import { ProtoFailure } from '../failure';
@@ -77,7 +77,7 @@ export async function decodeOptionalSinglePayload<T>(
7777
dataConverter: LoadedDataConverter,
7878
payload?: Payload | null | undefined
7979
): Promise<T | null | undefined> {
80-
const { payloadCodecs, payloadConverter } = dataConverter;
80+
const { payloadConverter, payloadCodecs } = dataConverter;
8181
const decoded = await decodeOptionalSingle(payloadCodecs, payload);
8282
if (decoded == null) return decoded;
8383
return payloadConverter.fromPayload(decoded);
@@ -94,15 +94,13 @@ export async function encodeToPayload(converter: LoadedDataConverter, value: unk
9494
/**
9595
* Run {@link PayloadConverter.toPayload} on an optional value, and then encode it.
9696
*/
97-
export async function encodeOptionalToPayload(
98-
converter: LoadedDataConverter,
97+
export function encodeOptionalToPayload(
98+
payloadConverter: PayloadConverter,
9999
value: unknown
100-
): Promise<Payload | null | undefined> {
100+
): Payload | null | undefined {
101101
if (value == null) return value;
102102

103-
const { payloadConverter, payloadCodecs } = converter;
104-
const payload = payloadConverter.toPayload(value);
105-
return await encodeSingle(payloadCodecs, payload);
103+
return payloadConverter.toPayload(value);
106104
}
107105

108106
/**

packages/common/src/internal-workflow/objects-helpers.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,33 @@ export function mergeObjects<T extends Record<string, any>>(
3535

3636
return changed ? (merged as T) : original;
3737
}
38+
39+
function isObject(item: any): item is Record<string, any> {
40+
return item && typeof item === 'object' && !Array.isArray(item);
41+
}
42+
43+
/**
44+
* Recursively merges two objects, returning a new object.
45+
*
46+
* Properties from `source` will overwrite properties on `target`.
47+
* Nested objects are merged recursively.
48+
*
49+
* Object fields in the returned object are references, as in,
50+
* the returned object is not completely fresh.
51+
*/
52+
export function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
53+
const output = { ...target };
54+
55+
if (isObject(target) && isObject(source)) {
56+
for (const key of Object.keys(source)) {
57+
const sourceValue = source[key];
58+
if (isObject(sourceValue) && key in target && isObject(target[key] as any)) {
59+
output[key as keyof T] = deepMerge(target[key], sourceValue);
60+
} else {
61+
(output as any)[key] = sourceValue;
62+
}
63+
}
64+
}
65+
66+
return output;
67+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { temporal } from '@temporalio/proto';
2+
import { PayloadConverter } from './converter/payload-converter';
3+
import { LoadedDataConverter } from './converter/data-converter';
4+
import { encodeOptionalToPayload, decodeOptionalSinglePayload, encodeOptionalSingle } from './internal-non-workflow';
5+
6+
/**
7+
* User metadata that can be attached to workflow commands.
8+
*/
9+
export interface UserMetadata {
10+
/** @experimental A fixed, single line summary of the command's purpose */
11+
staticSummary?: string;
12+
/** @experimental Fixed additional details about the command for longer-text description, can span multiple lines */
13+
staticDetails?: string;
14+
}
15+
16+
export function userMetadataToPayload(
17+
payloadConverter: PayloadConverter,
18+
staticSummary: string | undefined,
19+
staticDetails: string | undefined
20+
): temporal.api.sdk.v1.IUserMetadata | undefined {
21+
if (staticSummary == null && staticDetails == null) return undefined;
22+
23+
const summary = encodeOptionalToPayload(payloadConverter, staticSummary);
24+
const details = encodeOptionalToPayload(payloadConverter, staticDetails);
25+
26+
if (summary == null && details == null) return undefined;
27+
28+
return { summary, details };
29+
}
30+
31+
export async function encodeUserMetadata(
32+
dataConverter: LoadedDataConverter,
33+
staticSummary: string | undefined,
34+
staticDetails: string | undefined
35+
): Promise<temporal.api.sdk.v1.IUserMetadata | undefined> {
36+
if (staticSummary == null && staticDetails == null) return undefined;
37+
38+
const { payloadConverter, payloadCodecs } = dataConverter;
39+
const summary = await encodeOptionalSingle(
40+
payloadCodecs,
41+
await encodeOptionalToPayload(payloadConverter, staticSummary)
42+
);
43+
const details = await encodeOptionalSingle(
44+
payloadCodecs,
45+
await encodeOptionalToPayload(payloadConverter, staticDetails)
46+
);
47+
48+
if (summary == null && details == null) return undefined;
49+
50+
return { summary, details };
51+
}
52+
53+
export async function decodeUserMetadata(
54+
dataConverter: LoadedDataConverter,
55+
metadata: temporal.api.sdk.v1.IUserMetadata | undefined | null
56+
): Promise<UserMetadata> {
57+
const res = { staticSummary: undefined, staticDetails: undefined };
58+
if (metadata == null) return res;
59+
60+
const staticSummary = (await decodeOptionalSinglePayload<string>(dataConverter, metadata.summary)) ?? undefined;
61+
const staticDetails = (await decodeOptionalSinglePayload<string>(dataConverter, metadata.details)) ?? undefined;
62+
63+
if (staticSummary == null && staticDetails == null) return res;
64+
65+
return { staticSummary, staticDetails };
66+
}

0 commit comments

Comments
 (0)