@temporalio/common
Version:
Common library for code that's used across the Client, Worker, and/or Workflow
301 lines (272 loc) • 10.2 kB
text/typescript
import type { temporal } from '@temporalio/proto';
import { makeProtoEnumConverters } from './internal-workflow';
/** @deprecated: Use {@link TypedSearchAttributes} instead */
export type SearchAttributeValueOrReadonly = SearchAttributeValue | Readonly<SearchAttributeValue> | undefined; // eslint-disable-line @typescript-eslint/no-deprecated
/** @deprecated: Use {@link TypedSearchAttributes} instead */
export type SearchAttributes = Record<string, SearchAttributeValueOrReadonly>; // eslint-disable-line @typescript-eslint/no-deprecated
/** @deprecated: Use {@link TypedSearchAttributes} instead */
export type SearchAttributeValue = string[] | number[] | boolean[] | Date[];
export const SearchAttributeType = {
TEXT: 'TEXT',
KEYWORD: 'KEYWORD',
INT: 'INT',
DOUBLE: 'DOUBLE',
BOOL: 'BOOL',
DATETIME: 'DATETIME',
KEYWORD_LIST: 'KEYWORD_LIST',
} as const;
export type SearchAttributeType = (typeof SearchAttributeType)[keyof typeof SearchAttributeType];
// Note: encodeSearchAttributeIndexedValueType exported for use in tests to register search attributes
// ts-prune-ignore-next
export const [encodeSearchAttributeIndexedValueType, _] = makeProtoEnumConverters<
temporal.api.enums.v1.IndexedValueType,
typeof temporal.api.enums.v1.IndexedValueType,
keyof typeof temporal.api.enums.v1.IndexedValueType,
typeof SearchAttributeType,
'INDEXED_VALUE_TYPE_'
>(
{
[SearchAttributeType.TEXT]: 1,
[SearchAttributeType.KEYWORD]: 2,
[SearchAttributeType.INT]: 3,
[SearchAttributeType.DOUBLE]: 4,
[SearchAttributeType.BOOL]: 5,
[SearchAttributeType.DATETIME]: 6,
[SearchAttributeType.KEYWORD_LIST]: 7,
UNSPECIFIED: 0,
} as const,
'INDEXED_VALUE_TYPE_'
);
interface IndexedValueTypeMapping {
TEXT: string;
KEYWORD: string;
INT: number;
DOUBLE: number;
BOOL: boolean;
DATETIME: Date;
KEYWORD_LIST: string[];
}
export function isValidValueForType<T extends SearchAttributeType>(
type: T,
value: unknown
): value is IndexedValueTypeMapping[T] {
switch (type) {
case SearchAttributeType.TEXT:
case SearchAttributeType.KEYWORD:
return typeof value === 'string';
case SearchAttributeType.INT:
return Number.isInteger(value);
case SearchAttributeType.DOUBLE:
return typeof value === 'number';
case SearchAttributeType.BOOL:
return typeof value === 'boolean';
case SearchAttributeType.DATETIME:
return value instanceof Date;
case SearchAttributeType.KEYWORD_LIST:
return Array.isArray(value) && value.every((item) => typeof item === 'string');
default:
return false;
}
}
export interface SearchAttributeKey<T extends SearchAttributeType> {
name: string;
type: T;
}
export function defineSearchAttributeKey<T extends SearchAttributeType>(name: string, type: T): SearchAttributeKey<T> {
return { name, type };
}
class BaseSearchAttributeValue<T extends SearchAttributeType, V = IndexedValueTypeMapping[T]> {
private readonly _type: T;
private readonly _value: V;
constructor(type: T, value: V) {
this._type = type;
this._value = value;
}
get type(): T {
return this._type;
}
get value(): V {
return this._value;
}
}
// Internal type for class private data.
// Exported for use in payload conversion.
export class TypedSearchAttributeValue<T extends SearchAttributeType> extends BaseSearchAttributeValue<T> {}
// ts-prune-ignore-next
export class TypedSearchAttributeUpdateValue<T extends SearchAttributeType> extends BaseSearchAttributeValue<
T,
IndexedValueTypeMapping[T] | null
> {}
export type SearchAttributePair = {
[T in SearchAttributeType]: { key: SearchAttributeKey<T>; value: IndexedValueTypeMapping[T] };
}[SearchAttributeType];
export type SearchAttributeUpdatePair = {
[T in SearchAttributeType]: { key: SearchAttributeKey<T>; value: IndexedValueTypeMapping[T] | null };
}[SearchAttributeType];
export class TypedSearchAttributes {
private searchAttributes: Record<string, TypedSearchAttributeValue<SearchAttributeType>> = {};
constructor(initialAttributes?: SearchAttributePair[]) {
if (initialAttributes === undefined) return;
for (const pair of initialAttributes) {
if (pair.key.name in this.searchAttributes) {
throw new Error(`Duplicate search attribute key: ${pair.key.name}`);
}
this.searchAttributes[pair.key.name] = new TypedSearchAttributeValue(pair.key.type, pair.value);
}
}
get<T extends SearchAttributeType>(key: SearchAttributeKey<T>): IndexedValueTypeMapping[T] | undefined {
const attr = this.searchAttributes[key.name];
// Key not found or type mismatch.
if (attr === undefined || !isValidValueForType(key.type, attr.value)) {
return undefined;
}
return attr.value;
}
/** Returns a deep copy of the given TypedSearchAttributes instance */
copy(): TypedSearchAttributes {
const state: Record<string, TypedSearchAttributeValue<SearchAttributeType>> = {};
for (const [key, attr] of Object.entries(this.searchAttributes)) {
// Create a new instance with the same properties
let value = attr.value;
// For non-primitive types, create a deep copy
if (attr.value instanceof Date) {
value = new Date(attr.value);
} else if (Array.isArray(attr.value)) {
value = [...attr.value];
}
state[key] = new TypedSearchAttributeValue(attr.type, value);
}
// Create return value with manually assigned state.
const res = new TypedSearchAttributes();
res.searchAttributes = state;
return res;
}
/**
* @hidden
* Return JSON representation of this class as SearchAttributePair[]
* Default toJSON method is not used because it's JSON representation includes private state.
*/
toJSON(): SearchAttributePair[] {
return this.getAll();
}
/** Returns a copy of the current TypedSearchAttributes instance with the updated attributes. */
updateCopy(updates: SearchAttributeUpdatePair[]): TypedSearchAttributes {
// Create a deep copy of the current instance.
const res = this.copy();
// Apply updates.
res.update(updates);
return res;
}
// Performs direct mutation on the current instance.
private update(updates: SearchAttributeUpdatePair[]) {
// Apply updates.
for (const pair of updates) {
// Delete attribute.
if (pair.value === null) {
// Delete only if the update matches a key and type.
const attrVal = this.searchAttributes[pair.key.name];
if (attrVal && attrVal.type === pair.key.type) {
delete this.searchAttributes[pair.key.name];
}
continue;
}
// Add or update attribute.
this.searchAttributes[pair.key.name] = new TypedSearchAttributeValue(pair.key.type, pair.value);
}
}
getAll(): SearchAttributePair[] {
const res: SearchAttributePair[] = [];
for (const [key, attr] of Object.entries(this.searchAttributes)) {
const attrKey = { name: key, type: attr.type };
// Sanity check, should always be legal.
if (isValidValueForType(attrKey.type, attr.value)) {
res.push({ key: attrKey, value: attr.value } as SearchAttributePair);
}
}
return res;
}
static getKeyFromUntyped(
key: string,
value: SearchAttributeValueOrReadonly // eslint-disable-line @typescript-eslint/no-deprecated
): SearchAttributeKey<SearchAttributeType> | undefined {
if (value == null) {
return;
}
// Unpack single-element arrays.
const val = value.length === 1 ? value[0] : value;
switch (typeof val) {
case 'string':
// Check if val is an ISO string, if so, return a DATETIME key.
if (!isNaN(Date.parse(val)) && Date.parse(val) === new Date(val).getTime()) {
return { name: key, type: SearchAttributeType.DATETIME };
}
return { name: key, type: SearchAttributeType.TEXT };
case 'number':
return {
name: key,
type: Number.isInteger(val) ? SearchAttributeType.INT : SearchAttributeType.DOUBLE,
};
case 'boolean':
return { name: key, type: SearchAttributeType.BOOL };
case 'object':
if (val instanceof Date) {
return { name: key, type: SearchAttributeType.DATETIME };
}
if (Array.isArray(val) && val.every((item) => typeof item === 'string')) {
return { name: key, type: SearchAttributeType.KEYWORD_LIST };
}
return;
default:
return;
}
}
static toMetadataType(type: SearchAttributeType): string {
switch (type) {
case SearchAttributeType.TEXT:
return 'Text';
case SearchAttributeType.KEYWORD:
return 'Keyword';
case SearchAttributeType.INT:
return 'Int';
case SearchAttributeType.DOUBLE:
return 'Double';
case SearchAttributeType.BOOL:
return 'Bool';
case SearchAttributeType.DATETIME:
return 'Datetime';
case SearchAttributeType.KEYWORD_LIST:
return 'KeywordList';
default:
throw new Error(`Unknown search attribute type: ${type}`);
}
}
static toSearchAttributeType(type: string): SearchAttributeType | undefined {
// The type metadata is usually in PascalCase (e.g. "KeywordList") but in
// rare cases may be in SCREAMING_SNAKE_CASE (e.g. "INDEXED_VALUE_TYPE_KEYWORD_LIST").
switch (type) {
case 'Text':
case 'INDEXED_VALUE_TYPE_TEXT':
return SearchAttributeType.TEXT;
case 'Keyword':
case 'INDEXED_VALUE_TYPE_KEYWORD':
return SearchAttributeType.KEYWORD;
case 'Int':
case 'INDEXED_VALUE_TYPE_INT':
return SearchAttributeType.INT;
case 'Double':
case 'INDEXED_VALUE_TYPE_DOUBLE':
return SearchAttributeType.DOUBLE;
case 'Bool':
case 'INDEXED_VALUE_TYPE_BOOL':
return SearchAttributeType.BOOL;
case 'Datetime':
case 'INDEXED_VALUE_TYPE_DATETIME':
return SearchAttributeType.DATETIME;
case 'KeywordList':
case 'INDEXED_VALUE_TYPE_KEYWORD_LIST':
return SearchAttributeType.KEYWORD_LIST;
default:
return;
}
}
}