Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typed search attributes #1612

Merged
merged 24 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fee7333
Type enforcement for typed search attributes
THardy98 Jan 15, 2025
eee023e
improve doc strings
THardy98 Jan 24, 2025
f2399cc
formatting
THardy98 Jan 24, 2025
103b1b8
fix build on merge
THardy98 Jan 24, 2025
25b0722
API change
THardy98 Jan 28, 2025
78b74a4
added tests, fixed bugs caught by tests
THardy98 Feb 6, 2025
af0754e
linting and formatting
THardy98 Feb 6, 2025
3261083
test fixes
THardy98 Feb 6, 2025
0c3afe2
moving creation methods (creating keys, attribute pairs, etc.) to sta…
THardy98 Feb 7, 2025
ba05b76
remove testing npm scripts
THardy98 Feb 7, 2025
40f9117
address review comments
THardy98 Feb 23, 2025
a6dab0c
update comments/error strings
THardy98 Feb 26, 2025
a903315
formatting
THardy98 Feb 26, 2025
05c4277
reverted WorkflowInfo back to class type. Upsert fix for deletion cas…
THardy98 Feb 28, 2025
da6f84b
removed payload-search-attributes export
THardy98 Feb 28, 2025
ce0e0b3
fix lint.prune mistake
THardy98 Feb 28, 2025
5083ea7
address pr review comments
THardy98 Mar 12, 2025
e0b4cd4
jsdoc code snippet fixes, use search attribute creation helpers in test
THardy98 Mar 13, 2025
acc1010
eslint deprecation warning suppression
THardy98 Mar 13, 2025
f1d8aaa
remove createSearchAttributePair/updatePair helpers
THardy98 Mar 13, 2025
b9a0868
fix more lint suppressions after formatting
THardy98 Mar 13, 2025
5b75abd
remove dev test scripts
THardy98 Mar 13, 2025
6f72e04
Merge branch 'main' into typed-search-attributes
THardy98 Mar 13, 2025
64ea9b5
remove extra comma
THardy98 Mar 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/activity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,14 @@ export {
*
* @example
*
*```ts
* ```ts
*import { CompleteAsyncError } from '@temporalio/activity';
*
*export async function myActivity(): Promise<never> {
* // ...
* throw new CompleteAsyncError();
*}
*```
* ```
*/
@SymbolBasedInstanceOfError('CompleteAsyncError')
export class CompleteAsyncError extends Error {}
Expand Down
16 changes: 6 additions & 10 deletions packages/client/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ServiceError as GrpcServiceError, status as grpcStatus } from '@grpc/grpc-js';
import { LoadedDataConverter, NamespaceNotFoundError } from '@temporalio/common';
import {
LoadedDataConverter,
mapFromPayloads,
NamespaceNotFoundError,
decodeSearchAttributes,
decodeTypedSearchAttributes,
searchAttributePayloadConverter,
SearchAttributes,
} from '@temporalio/common';
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { Replace } from '@temporalio/common/lib/type-helpers';
import { optionalTsToDate, requiredTsToDate } from '@temporalio/common/lib/time';
import { decodeMapFromPayloads } from '@temporalio/common/lib/internal-non-workflow/codec-helpers';
Expand Down Expand Up @@ -71,11 +70,8 @@ export async function executionInfoFromRaw<T>(
executionTime: optionalTsToDate(raw.executionTime),
closeTime: optionalTsToDate(raw.closeTime),
memo: await decodeMapFromPayloads(dataConverter, raw.memo?.fields),
searchAttributes: Object.fromEntries(
Object.entries(
mapFromPayloads(searchAttributePayloadConverter, raw.searchAttributes?.indexedFields ?? {}) as SearchAttributes
).filter(([_, v]) => v && v.length > 0) // Filter out empty arrays returned by pre 1.18 servers
),
searchAttributes: decodeSearchAttributes(raw.searchAttributes?.indexedFields),
typedSearchAttributes: decodeTypedSearchAttributes(raw.searchAttributes?.indexedFields),
parentExecution: raw.parentExecution
? {
workflowId: raw.parentExecution.workflowId!,
Expand Down
25 changes: 16 additions & 9 deletions packages/client/src/schedule-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { status as grpcStatus } from '@grpc/grpc-js';
import { v4 as uuid4 } from 'uuid';
import { mapToPayloads, searchAttributePayloadConverter, Workflow } from '@temporalio/common';
import { Workflow } from '@temporalio/common';
import {
decodeSearchAttributes,
decodeTypedSearchAttributes,
encodeUnifiedSearchAttributes,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { composeInterceptors, Headers } from '@temporalio/common/lib/interceptors';
import {
encodeMapToPayloads,
Expand Down Expand Up @@ -39,7 +44,6 @@ import {
decodeScheduleRecentActions,
decodeScheduleRunningActions,
decodeScheduleSpec,
decodeSearchAttributes,
encodeScheduleAction,
encodeSchedulePolicies,
encodeScheduleSpec,
Expand Down Expand Up @@ -238,11 +242,12 @@ export class ScheduleClient extends BaseClient {
state: encodeScheduleState(opts.state),
},
memo: opts.memo ? { fields: await encodeMapToPayloads(this.dataConverter, opts.memo) } : undefined,
searchAttributes: opts.searchAttributes
? {
indexedFields: mapToPayloads(searchAttributePayloadConverter, opts.searchAttributes),
}
: undefined,
searchAttributes:
opts.searchAttributes || opts.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? {
indexedFields: encodeUnifiedSearchAttributes(opts.searchAttributes, opts.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
}
: undefined,
initialPatch: {
triggerImmediately: opts.state?.triggerImmediately
? { overlapPolicy: temporal.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL }
Expand Down Expand Up @@ -388,7 +393,8 @@ export class ScheduleClient extends BaseClient {
workflowType: raw.info.workflowType.name,
},
memo: await decodeMapFromPayloads(this.dataConverter, raw.memo?.fields),
searchAttributes: decodeSearchAttributes(raw.searchAttributes),
searchAttributes: decodeSearchAttributes(raw.searchAttributes?.indexedFields),
typedSearchAttributes: decodeTypedSearchAttributes(raw.searchAttributes?.indexedFields),
state: {
paused: raw.info?.paused === true,
note: raw.info?.notes ?? undefined,
Expand Down Expand Up @@ -425,7 +431,8 @@ export class ScheduleClient extends BaseClient {
spec: decodeScheduleSpec(raw.schedule.spec),
action: await decodeScheduleAction(this.client.dataConverter, raw.schedule.action),
memo: await decodeMapFromPayloads(this.client.dataConverter, raw.memo?.fields),
searchAttributes: decodeSearchAttributes(raw.searchAttributes),
searchAttributes: decodeSearchAttributes(raw.searchAttributes?.indexedFields),
typedSearchAttributes: decodeTypedSearchAttributes(raw.searchAttributes?.indexedFields),
policies: {
// 'overlap' should never be missing on describe, as the server will replace UNSPECIFIED by an actual value
overlap: decodeScheduleOverlapPolicy(raw.schedule.policies?.overlapPolicy) ?? ScheduleOverlapPolicy.SKIP,
Expand Down
46 changes: 13 additions & 33 deletions packages/client/src/schedule-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import Long from 'long'; // eslint-disable-line import/no-named-as-default
import { compileRetryPolicy, decompileRetryPolicy, extractWorkflowType, LoadedDataConverter } from '@temporalio/common';
import {
compileRetryPolicy,
decompileRetryPolicy,
extractWorkflowType,
LoadedDataConverter,
mapFromPayloads,
mapToPayloads,
searchAttributePayloadConverter,
SearchAttributes,
} from '@temporalio/common';
encodeUnifiedSearchAttributes,
decodeSearchAttributes,
decodeTypedSearchAttributes,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { Headers } from '@temporalio/common/lib/interceptors';
import {
decodeArrayFromPayloads,
Expand Down Expand Up @@ -260,11 +256,12 @@ export async function encodeScheduleAction(
workflowTaskTimeout: msOptionalToTs(action.workflowTaskTimeout),
retryPolicy: action.retry ? compileRetryPolicy(action.retry) : undefined,
memo: action.memo ? { fields: await encodeMapToPayloads(dataConverter, action.memo) } : undefined,
searchAttributes: action.searchAttributes
? {
indexedFields: mapToPayloads(searchAttributePayloadConverter, action.searchAttributes),
}
: undefined,
searchAttributes:
action.searchAttributes || action.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? {
indexedFields: encodeUnifiedSearchAttributes(action.searchAttributes, action.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
}
: undefined,
header: { fields: headers },
},
};
Expand Down Expand Up @@ -326,14 +323,8 @@ export async function decodeScheduleAction(
args: await decodeArrayFromPayloads(dataConverter, pb.startWorkflow.input?.payloads),
memo: await decodeMapFromPayloads(dataConverter, pb.startWorkflow.memo?.fields),
retry: decompileRetryPolicy(pb.startWorkflow.retryPolicy),
searchAttributes: Object.fromEntries(
Object.entries(
mapFromPayloads(
searchAttributePayloadConverter,
pb.startWorkflow.searchAttributes?.indexedFields ?? {}
) as SearchAttributes
)
),
searchAttributes: decodeSearchAttributes(pb.startWorkflow.searchAttributes?.indexedFields),
typedSearchAttributes: decodeTypedSearchAttributes(pb.startWorkflow.searchAttributes?.indexedFields),
workflowExecutionTimeout: optionalTsToMs(pb.startWorkflow.workflowExecutionTimeout),
workflowRunTimeout: optionalTsToMs(pb.startWorkflow.workflowRunTimeout),
workflowTaskTimeout: optionalTsToMs(pb.startWorkflow.workflowTaskTimeout),
Expand All @@ -342,17 +333,6 @@ export async function decodeScheduleAction(
throw new TypeError('Unsupported schedule action');
}

export function decodeSearchAttributes(
pb: temporal.api.common.v1.ISearchAttributes | undefined | null
): SearchAttributes {
if (!pb?.indexedFields) return {};
return Object.fromEntries(
Object.entries(mapFromPayloads(searchAttributePayloadConverter, pb.indexedFields) as SearchAttributes).filter(
([_, v]) => v && v.length > 0
) // Filter out empty arrays returned by pre 1.18 servers
);
}

export function decodeScheduleRunningActions(
pb?: temporal.api.common.v1.IWorkflowExecution[] | null
): ScheduleExecutionStartWorkflowActionResult[] {
Expand Down
53 changes: 44 additions & 9 deletions packages/client/src/schedule-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { checkExtends, Replace } from '@temporalio/common/lib/type-helpers';
import { Duration, SearchAttributes, Workflow } from '@temporalio/common';
import { Duration, SearchAttributes, Workflow, TypedSearchAttributes, SearchAttributePair } from '@temporalio/common';
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
import type { temporal } from '@temporalio/proto';
import { WorkflowStartOptions } from './workflow-options';
Expand Down Expand Up @@ -70,8 +70,21 @@ export interface ScheduleOptions<A extends ScheduleOptionsAction = ScheduleOptio
* https://docs.temporal.io/docs/typescript/search-attributes
*
* Values are always converted using {@link JsonPayloadConverter}, even when a custom Data Converter is provided.
*
* @deprecated Use {@link typedSearchAttributes} instead.
*/
searchAttributes?: SearchAttributes;
searchAttributes?: SearchAttributes; // eslint-disable-line deprecation/deprecation

/**
* Additional indexed information attached to the Schedule. More info:
* https://docs.temporal.io/docs/typescript/search-attributes
*
* Values are always converted using {@link JsonPayloadConverter}, even when a custom Data Converter is provided.
*
* If both {@link searchAttributes} and {@link typedSearchAttributes} are provided, conflicting keys will be overwritten
* by {@link typedSearchAttributes}.
*/
typedSearchAttributes?: SearchAttributePair[] | TypedSearchAttributes;

/**
* The initial state of the schedule, right after creation or update.
Expand Down Expand Up @@ -129,7 +142,7 @@ export type CompiledScheduleOptions = Replace<
* The specification of an updated Schedule, as expected by {@link ScheduleHandle.update}.
*/
export type ScheduleUpdateOptions<A extends ScheduleOptionsAction = ScheduleOptionsAction> = Replace<
Omit<ScheduleOptions, 'scheduleId' | 'memo' | 'searchAttributes'>,
Omit<ScheduleOptions, 'scheduleId' | 'memo' | 'searchAttributes' | 'typedSearchAttributes'>,
{
action: A;
state: Omit<ScheduleOptions['state'], 'triggerImmediately' | 'backfill'>;
Expand Down Expand Up @@ -172,12 +185,22 @@ export interface ScheduleSummary {
memo?: Record<string, unknown>;

/**
* Additional indexed information attached to the Schedule.
* More info: https://docs.temporal.io/docs/typescript/search-attributes
* Additional indexed information attached to the Schedule. More info:
* https://docs.temporal.io/docs/typescript/search-attributes
*
* Values are always converted using {@link JsonPayloadConverter}, even when a custom Data Converter is provided.
*
* @deprecated Use {@link typedSearchAttributes} instead.
*/
searchAttributes?: SearchAttributes;
searchAttributes?: SearchAttributes; // eslint-disable-line deprecation/deprecation

/**
* Additional indexed information attached to the Schedule. More info:
* https://docs.temporal.io/docs/typescript/search-attributes
*
* Values are always converted using {@link JsonPayloadConverter}, even when a custom Data Converter is provided.
*/
typedSearchAttributes?: TypedSearchAttributes;

state: {
/**
Expand Down Expand Up @@ -284,12 +307,22 @@ export type ScheduleDescription = {
memo?: Record<string, unknown>;

/**
* Additional indexed information attached to the Schedule.
* More info: https://docs.temporal.io/docs/typescript/search-attributes
* Additional indexed information attached to the Schedule. More info:
* https://docs.temporal.io/docs/typescript/search-attributes
*
* Values are always converted using {@link JsonPayloadConverter}, even when a custom Data Converter is provided.
*
* @deprecated Use {@link typedSearchAttributes} instead.
*/
searchAttributes: SearchAttributes; // eslint-disable-line deprecation/deprecation

/**
* Additional indexed information attached to the Schedule. More info:
* https://docs.temporal.io/docs/typescript/search-attributes
*
* Values are always converted using {@link JsonPayloadConverter}, even when a custom Data Converter is provided.
*/
searchAttributes: SearchAttributes;
typedSearchAttributes: TypedSearchAttributes;

state: {
/**
Expand Down Expand Up @@ -745,6 +778,7 @@ export type ScheduleOptionsStartWorkflowAction<W extends Workflow> = {
| 'args'
| 'memo'
| 'searchAttributes'
| 'typedSearchAttributes'
| 'retry'
| 'workflowExecutionTimeout'
| 'workflowRunTimeout'
Expand Down Expand Up @@ -776,6 +810,7 @@ export type ScheduleDescriptionStartWorkflowAction = ScheduleSummaryStartWorkflo
| 'args'
| 'memo'
| 'searchAttributes'
| 'typedSearchAttributes'
| 'retry'
| 'workflowExecutionTimeout'
| 'workflowRunTimeout'
Expand Down
8 changes: 5 additions & 3 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as grpc from '@grpc/grpc-js';
import type { SearchAttributes, SearchAttributeValue } from '@temporalio/common';
import type { TypedSearchAttributes, SearchAttributes, SearchAttributeValue } from '@temporalio/common';
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
import * as proto from '@temporalio/proto';
import { Replace } from '@temporalio/common/lib/type-helpers';
Expand Down Expand Up @@ -47,7 +47,9 @@ export interface WorkflowExecutionInfo {
executionTime?: Date;
closeTime?: Date;
memo?: Record<string, unknown>;
searchAttributes: SearchAttributes;
/** @deprecated Use {@link typedSearchAttributes} instead. */
searchAttributes: SearchAttributes; // eslint-disable-line deprecation/deprecation
typedSearchAttributes: TypedSearchAttributes;
parentExecution?: Required<proto.temporal.api.common.v1.IWorkflowExecution>;
raw: RawWorkflowExecutionInfo;
}
Expand All @@ -56,7 +58,7 @@ export interface CountWorkflowExecution {
count: number;
groups: {
count: number;
groupValues: SearchAttributeValue[];
groupValues: SearchAttributeValue[]; // eslint-disable-line deprecation/deprecation
}[];
}

Expand Down
26 changes: 14 additions & 12 deletions packages/client/src/workflow-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import {
BaseWorkflowHandle,
CancelledFailure,
compileRetryPolicy,
mapToPayloads,
HistoryAndWorkflowId,
QueryDefinition,
RetryState,
searchAttributePayloadConverter,
SignalDefinition,
UpdateDefinition,
TerminatedFailure,
Expand All @@ -25,6 +23,7 @@ import {
encodeWorkflowIdConflictPolicy,
WorkflowIdConflictPolicy,
} from '@temporalio/common';
import { encodeUnifiedSearchAttributes } from '@temporalio/common/lib/converter/payload-search-attributes';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { History } from '@temporalio/common/lib/proto-utils';
import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers';
Expand Down Expand Up @@ -1218,11 +1217,12 @@ export class WorkflowClient extends BaseClient {
workflowStartDelay: options.startDelay,
retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
memo: options.memo ? { fields: await encodeMapToPayloads(this.dataConverter, options.memo) } : undefined,
searchAttributes: options.searchAttributes
? {
indexedFields: mapToPayloads(searchAttributePayloadConverter, options.searchAttributes),
}
: undefined,
searchAttributes:
options.searchAttributes || options.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? {
indexedFields: encodeUnifiedSearchAttributes(options.searchAttributes, options.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
}
: undefined,
cronSchedule: options.cronSchedule,
header: { fields: headers },
};
Expand Down Expand Up @@ -1265,6 +1265,7 @@ export class WorkflowClient extends BaseClient {
protected async createStartWorkflowRequest(input: WorkflowStartInput): Promise<StartWorkflowExecutionRequest> {
const { options: opts, workflowType, headers } = input;
const { identity, namespace } = this.options;

return {
namespace,
identity,
Expand All @@ -1284,11 +1285,12 @@ export class WorkflowClient extends BaseClient {
workflowStartDelay: opts.startDelay,
retryPolicy: opts.retry ? compileRetryPolicy(opts.retry) : undefined,
memo: opts.memo ? { fields: await encodeMapToPayloads(this.dataConverter, opts.memo) } : undefined,
searchAttributes: opts.searchAttributes
? {
indexedFields: mapToPayloads(searchAttributePayloadConverter, opts.searchAttributes),
}
: undefined,
searchAttributes:
opts.searchAttributes || opts.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? {
indexedFields: encodeUnifiedSearchAttributes(opts.searchAttributes, opts.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
}
: undefined,
cronSchedule: opts.cronSchedule,
header: { fields: headers },
};
Expand Down
Loading
Loading