Skip to content

Commit 9bceb64

Browse files
authored
Merge branch 'main' into user-metadata
2 parents 6af2e2c + c6e7c88 commit 9bceb64

9 files changed

+215
-33
lines changed

packages/client/src/schedule-client.ts

+6
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ export class ScheduleClient extends BaseClient {
307307
},
308308
identity: this.options.identity,
309309
requestId: uuid4(),
310+
searchAttributes:
311+
opts.searchAttributes || opts.typedSearchAttributes // eslint-disable-line deprecation/deprecation
312+
? {
313+
indexedFields: encodeUnifiedSearchAttributes(opts.searchAttributes, opts.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
314+
}
315+
: undefined,
310316
};
311317
try {
312318
return await this.workflowService.updateSchedule(req);

packages/client/src/schedule-types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export type CompiledScheduleOptions = Replace<
142142
* The specification of an updated Schedule, as expected by {@link ScheduleHandle.update}.
143143
*/
144144
export type ScheduleUpdateOptions<A extends ScheduleOptionsAction = ScheduleOptionsAction> = Replace<
145-
Omit<ScheduleOptions, 'scheduleId' | 'memo' | 'searchAttributes' | 'typedSearchAttributes'>,
145+
Omit<ScheduleOptions, 'scheduleId' | 'memo'>,
146146
{
147147
action: A;
148148
state: Omit<ScheduleOptions['state'], 'triggerImmediately' | 'backfill'>;

packages/core-bridge/ts/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { LogLevel, Duration } from '@temporalio/common';
1+
import { LogLevel, Duration, SearchAttributeType } from '@temporalio/common';
22
import type { TLSConfig, ProxyConfig, HttpConnectProxyConfig } from '@temporalio/common/lib/internal-non-workflow';
33
import { WorkerTuner } from './worker-tuner';
4+
import { SearchAttributeKey } from '@temporalio/common/src/search-attributes';
45

56
export {
67
WorkerTuner,
@@ -511,6 +512,10 @@ export interface DevServerConfig {
511512
* be supported in the future.
512513
*/
513514
extraArgs?: string[];
515+
/**
516+
* Search attributes to be registered with the dev server.
517+
*/
518+
searchAttributes?: SearchAttributeKey<SearchAttributeType>[];
514519
}
515520

516521
/**

packages/test/src/helpers-integration-multi-codec.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
createLocalTestEnvironment,
1111
makeConfigurableEnvironmentTestFn,
1212
} from './helpers-integration';
13-
import { ByteSkewerPayloadCodec, registerDefaultCustomSearchAttributes, Worker } from './helpers';
13+
import { ByteSkewerPayloadCodec, Worker } from './helpers';
1414

1515
// Note: re-export shared workflows (or long workflows)
1616
export * from './workflows';
@@ -43,7 +43,6 @@ export function makeTestFn(makeBundle: () => Promise<WorkflowBundle>): TestFn<Te
4343
const env = await createLocalTestEnvironment({
4444
client: { dataConverter },
4545
});
46-
await registerDefaultCustomSearchAttributes(env.connection);
4746

4847
configs.push({
4948
loadedDataConverter,

packages/test/src/helpers-integration.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,9 @@ import {
2727
} from '@temporalio/worker';
2828
import * as workflow from '@temporalio/workflow';
2929
import { temporal } from '@temporalio/proto';
30+
import { defineSearchAttributeKey, SearchAttributeType } from '@temporalio/common/lib/search-attributes';
3031
import { ConnectionInjectorInterceptor } from './activities/interceptors';
31-
import {
32-
Worker,
33-
TestWorkflowEnvironment,
34-
test as anyTest,
35-
bundlerOptions,
36-
registerDefaultCustomSearchAttributes,
37-
} from './helpers';
32+
import { Worker, TestWorkflowEnvironment, test as anyTest, bundlerOptions } from './helpers';
3833

3934
export interface Context {
4035
env: TestWorkflowEnvironment;
@@ -89,12 +84,22 @@ export async function createTestWorkflowBundle({
8984
});
9085
}
9186

87+
export const defaultSAKeys = {
88+
CustomIntField: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT),
89+
CustomBoolField: defineSearchAttributeKey('CustomBoolField', SearchAttributeType.BOOL),
90+
CustomKeywordField: defineSearchAttributeKey('CustomKeywordField', SearchAttributeType.KEYWORD),
91+
CustomTextField: defineSearchAttributeKey('CustomTextField', SearchAttributeType.TEXT),
92+
CustomDatetimeField: defineSearchAttributeKey('CustomDatetimeField', SearchAttributeType.DATETIME),
93+
CustomDoubleField: defineSearchAttributeKey('CustomDoubleField', SearchAttributeType.DOUBLE),
94+
};
95+
9296
export async function createLocalTestEnvironment(
9397
opts?: LocalTestWorkflowEnvironmentOptions
9498
): Promise<TestWorkflowEnvironment> {
9599
return await TestWorkflowEnvironment.createLocal({
96100
...(opts || {}), // Use provided options or default to an empty object
97101
server: {
102+
searchAttributes: Object.values(defaultSAKeys),
98103
...(opts?.server || {}), // Use provided server options or default to an empty object
99104
extraArgs: [
100105
...defaultDynamicConfigOptions.flatMap((opt) => ['--dynamic-config-value', opt]),
@@ -130,7 +135,6 @@ export function makeTestFunction(opts: {
130135
recordedLogs: opts.recordedLogs,
131136
createTestContext: async (_t: ExecutionContext): Promise<Context> => {
132137
const env = await createLocalTestEnvironment(opts.workflowEnvironmentOpts);
133-
await registerDefaultCustomSearchAttributes(env.connection);
134138
return {
135139
workflowBundle: await createTestWorkflowBundle({
136140
workflowsPath: opts.workflowsPath,

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

+32-2
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ import { SdkFlags } from '@temporalio/workflow/lib/flags';
1313
import {
1414
ActivityCancellationType,
1515
ApplicationFailure,
16+
defineSearchAttributeKey,
1617
JsonPayloadConverter,
18+
SearchAttributePair,
19+
SearchAttributeType,
20+
TypedSearchAttributes,
1721
WorkflowExecutionAlreadyStartedError,
1822
} from '@temporalio/common';
1923
import { temporal } from '@temporalio/proto';
2024
import { signalSchedulingWorkflow } from './activities/helpers';
2125
import { activityStartedSignal } from './workflows/definitions';
2226
import * as workflows from './workflows';
23-
import { Context, helpers, makeTestFunction } from './helpers-integration';
27+
import { Context, createLocalTestEnvironment, helpers, makeTestFunction } from './helpers-integration';
2428
import { overrideSdkInternalFlag } from './mock-internal-flags';
2529
import { asSdkLoggerSink, loadHistory, RUN_TIME_SKIPPING_TESTS } from './helpers';
2630

@@ -1310,6 +1314,32 @@ test('Count workflow executions', async (t) => {
13101314
});
13111315
});
13121316

1317+
test('can register search attributes to dev server', async (t) => {
1318+
const key = defineSearchAttributeKey('new-search-attr', SearchAttributeType.INT);
1319+
const newSearchAttribute: SearchAttributePair = { key, value: 12 };
1320+
1321+
// Create new test environment with search attribute registered.
1322+
const env = await createLocalTestEnvironment({
1323+
server: {
1324+
searchAttributes: [key],
1325+
},
1326+
});
1327+
1328+
const newClient = env.client;
1329+
// Expect workflow with search attribute to start without error.
1330+
const handle = await newClient.workflow.start(completableWorkflow, {
1331+
args: [true],
1332+
workflowId: randomUUID(),
1333+
taskQueue: 'new-env-tq',
1334+
typedSearchAttributes: [newSearchAttribute],
1335+
});
1336+
// Expect workflow description to have search attribute.
1337+
const desc = await handle.describe();
1338+
t.deepEqual(desc.typedSearchAttributes, new TypedSearchAttributes([newSearchAttribute]));
1339+
t.deepEqual(desc.searchAttributes, { 'new-search-attr': [12] }); // eslint-disable-line deprecation/deprecation
1340+
await env.teardown();
1341+
});
1342+
13131343
export async function userMetadataWorkflow(): Promise<string> {
13141344
let done = false;
13151345
const signalDef = defineSignal('done');
@@ -1381,4 +1411,4 @@ test('User metadata on workflow, timer, activity', async (t) => {
13811411
t.deepEqual(wfMetadata.definition?.queryDefinitions?.length, 3); // default queries
13821412
t.deepEqual(wfMetadata.currentDetails, 'current wf details');
13831413
});
1384-
});
1414+
});

packages/test/src/test-schedules.ts

+143-19
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@ import {
1010
ScheduleHandle,
1111
ScheduleSummary,
1212
ScheduleUpdateOptions,
13+
ScheduleDescription,
1314
} from '@temporalio/client';
1415
import { msToNumber } from '@temporalio/common/lib/time';
15-
import { SearchAttributes, SearchAttributeType, TypedSearchAttributes } from '@temporalio/common';
16-
import { registerDefaultCustomSearchAttributes, RUN_INTEGRATION_TESTS } from './helpers';
16+
import {
17+
SearchAttributeType,
18+
SearchAttributes,
19+
TypedSearchAttributes,
20+
defineSearchAttributeKey,
21+
} from '@temporalio/common';
22+
import { registerDefaultCustomSearchAttributes, RUN_INTEGRATION_TESTS, waitUntil } from './helpers';
23+
import { defaultSAKeys } from './helpers-integration';
1724

1825
export interface Context {
1926
client: Client;
@@ -168,9 +175,7 @@ if (RUN_INTEGRATION_TESTS) {
168175
searchAttributes: {
169176
CustomKeywordField: ['test-value2'],
170177
},
171-
typedSearchAttributes: new TypedSearchAttributes([
172-
{ key: { name: 'CustomIntField', type: SearchAttributeType.INT }, value: 42 },
173-
]),
178+
typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]),
174179
},
175180
});
176181

@@ -188,8 +193,8 @@ if (RUN_INTEGRATION_TESTS) {
188193
t.deepEqual(
189194
describedSchedule.action.typedSearchAttributes,
190195
new TypedSearchAttributes([
191-
{ key: { name: 'CustomIntField', type: SearchAttributeType.INT }, value: 42 },
192-
{ key: { name: 'CustomKeywordField', type: SearchAttributeType.KEYWORD }, value: 'test-value2' },
196+
{ key: defaultSAKeys.CustomIntField, value: 42 },
197+
{ key: defaultSAKeys.CustomKeywordField, value: 'test-value2' },
193198
])
194199
);
195200
} finally {
@@ -216,9 +221,7 @@ if (RUN_INTEGRATION_TESTS) {
216221
searchAttributes: {
217222
CustomKeywordField: ['test-value2'],
218223
},
219-
typedSearchAttributes: new TypedSearchAttributes([
220-
{ key: { name: 'CustomIntField', type: SearchAttributeType.INT }, value: 42 },
221-
]),
224+
typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]),
222225
},
223226
});
224227

@@ -237,8 +240,8 @@ if (RUN_INTEGRATION_TESTS) {
237240
t.deepEqual(
238241
describedSchedule.action.typedSearchAttributes,
239242
new TypedSearchAttributes([
240-
{ key: { name: 'CustomIntField', type: SearchAttributeType.INT }, value: 42 },
241-
{ key: { name: 'CustomKeywordField', type: SearchAttributeType.KEYWORD }, value: 'test-value2' },
243+
{ key: defaultSAKeys.CustomIntField, value: 42 },
244+
{ key: defaultSAKeys.CustomKeywordField, value: 'test-value2' },
242245
])
243246
);
244247
} finally {
@@ -351,9 +354,7 @@ if (RUN_INTEGRATION_TESTS) {
351354
searchAttributes: {
352355
CustomKeywordField: ['test-value2'],
353356
},
354-
typedSearchAttributes: new TypedSearchAttributes([
355-
{ key: { name: 'CustomIntField', type: SearchAttributeType.INT }, value: 42 },
356-
]),
357+
typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]),
357358
},
358359
});
359360

@@ -598,9 +599,7 @@ if (RUN_INTEGRATION_TESTS) {
598599
taskQueue,
599600
},
600601
searchAttributes,
601-
typedSearchAttributes: new TypedSearchAttributes([
602-
{ key: { name: 'CustomIntField', type: SearchAttributeType.INT }, value: 42 },
603-
]),
602+
typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]),
604603
})
605604
);
606605
}
@@ -759,6 +758,131 @@ if (RUN_INTEGRATION_TESTS) {
759758
}
760759
});
761760

761+
=======
762+
test.serial('Can update search attributes of a schedule', async (t) => {
763+
const { client } = t.context;
764+
const scheduleId = `can-update-search-attributes-of-schedule-${randomUUID()}`;
765+
766+
// Helper to wait for search attribute changes to propagate.
767+
const waitForAttributeChange = async (
768+
handle: ScheduleHandle,
769+
attributeName: string,
770+
shouldExist: boolean
771+
): Promise<ScheduleDescription> => {
772+
await waitUntil(async () => {
773+
const desc = await handle.describe();
774+
const exists =
775+
desc.typedSearchAttributes.getAll().find((pair) => pair.key.name === attributeName) !== undefined;
776+
return exists === shouldExist;
777+
}, 300);
778+
return await handle.describe();
779+
};
780+
781+
// Create a schedule with search attributes.
782+
const handle = await client.schedule.create({
783+
scheduleId,
784+
spec: {
785+
calendars: [{ hour: { start: 2, end: 7, step: 1 } }],
786+
},
787+
action: {
788+
type: 'startWorkflow',
789+
workflowType: dummyWorkflow,
790+
taskQueue,
791+
},
792+
searchAttributes: {
793+
CustomKeywordField: ['keyword-one'],
794+
},
795+
typedSearchAttributes: [{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 1 }],
796+
});
797+
798+
// Check the search attributes are part of the schedule description.
799+
const desc = await handle.describe();
800+
// eslint-disable-next-line deprecation/deprecation
801+
t.deepEqual(desc.searchAttributes, {
802+
CustomKeywordField: ['keyword-one'],
803+
CustomIntField: [1],
804+
});
805+
t.deepEqual(
806+
desc.typedSearchAttributes,
807+
new TypedSearchAttributes([
808+
{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 1 },
809+
{ key: defineSearchAttributeKey('CustomKeywordField', SearchAttributeType.KEYWORD), value: 'keyword-one' },
810+
])
811+
);
812+
813+
// Perform a series of updates to schedule's search attributes.
814+
try {
815+
// Update existing search attributes, add new ones.
816+
await handle.update((desc) => ({
817+
...desc,
818+
searchAttributes: {
819+
CustomKeywordField: ['keyword-two'],
820+
// Add a new search attribute.
821+
CustomDoubleField: [1.5],
822+
},
823+
typedSearchAttributes: [
824+
{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 2 },
825+
// Add a new typed search attribute.
826+
{ key: defineSearchAttributeKey('CustomTextField', SearchAttributeType.TEXT), value: 'new-text' },
827+
],
828+
}));
829+
830+
let desc = await waitForAttributeChange(handle, 'CustomTextField', true);
831+
// eslint-disable-next-line deprecation/deprecation
832+
t.deepEqual(desc.searchAttributes, {
833+
CustomKeywordField: ['keyword-two'],
834+
CustomIntField: [2],
835+
CustomDoubleField: [1.5],
836+
CustomTextField: ['new-text'],
837+
});
838+
t.deepEqual(
839+
desc.typedSearchAttributes,
840+
new TypedSearchAttributes([
841+
{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 2 },
842+
{ key: defineSearchAttributeKey('CustomKeywordField', SearchAttributeType.KEYWORD), value: 'keyword-two' },
843+
{ key: defineSearchAttributeKey('CustomTextField', SearchAttributeType.TEXT), value: 'new-text' },
844+
{ key: defineSearchAttributeKey('CustomDoubleField', SearchAttributeType.DOUBLE), value: 1.5 },
845+
])
846+
);
847+
848+
// Update and remove some search attributes. We remove a search attribute by omitting an existing key from the update.
849+
await handle.update((desc) => ({
850+
...desc,
851+
searchAttributes: {
852+
CustomKeywordField: ['keyword-three'],
853+
},
854+
typedSearchAttributes: [{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 3 }],
855+
}));
856+
857+
desc = await waitForAttributeChange(handle, 'CustomTextField', false);
858+
// eslint-disable-next-line deprecation/deprecation
859+
t.deepEqual(desc.searchAttributes, {
860+
CustomKeywordField: ['keyword-three'],
861+
CustomIntField: [3],
862+
});
863+
t.deepEqual(
864+
desc.typedSearchAttributes,
865+
new TypedSearchAttributes([
866+
{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 3 },
867+
{ key: defineSearchAttributeKey('CustomKeywordField', SearchAttributeType.KEYWORD), value: 'keyword-three' },
868+
])
869+
);
870+
871+
// Remove all search attributes.
872+
await handle.update((desc) => ({
873+
...desc,
874+
searchAttributes: {},
875+
typedSearchAttributes: [],
876+
}));
877+
878+
desc = await waitForAttributeChange(handle, 'CustomIntField', false);
879+
t.deepEqual(desc.searchAttributes, {}); // eslint-disable-line deprecation/deprecation
880+
t.deepEqual(desc.typedSearchAttributes, new TypedSearchAttributes([]));
881+
} finally {
882+
await handle.delete();
883+
}
884+
});
885+
762886
test.serial('User metadata on schedule', async (t) => {
763887
const { client } = t.context;
764888
const scheduleId = `schedule-with-user-metadata-${randomUUID()}`;
@@ -769,7 +893,7 @@ if (RUN_INTEGRATION_TESTS) {
769893
type: 'startWorkflow',
770894
workflowType: dummyWorkflow,
771895
taskQueue,
772-
staticSummary: 'schedule static summary',
896+
staticSummary: 'schedule static summary',
773897
staticDetails: 'schedule static details',
774898
},
775899
});

0 commit comments

Comments
 (0)