Skip to content

Commit a7296eb

Browse files
committed
refactor(search-attributes): poc generated schema usage
Also: Refactored the `fetchSearchAttributesForNamespace` function to enhance readability and maintainability. Introduced helper functions for parsing responses, mutating attributes, and handling errors. Changes include: - Replaced inline logic with `parsedResponse` and `mutateSearchAttributes`. - Added `mapEntries` utility for transforming object entries. - Updated types to align with the new schema-based approach. - Removed redundant `ListSearchAttributesResponse` type. This improves code clarity and aligns with schema validation practices.
1 parent 58060af commit a7296eb

File tree

5 files changed

+180
-42
lines changed

5 files changed

+180
-42
lines changed
Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,80 @@
1-
import type { SearchAttributesResponse } from '$lib/types/workflows';
2-
import { requestFromAPI } from '$lib/utilities/request-from-api';
1+
import { ListSearchAttributesResponse } from '@temporalio/schemas';
2+
3+
import type { SearchAttributesResponseHumanized } from '$lib/types/workflows';
4+
import { mapEntries } from '$lib/utilities/map-entries';
5+
import {
6+
isTemporalAPIError,
7+
requestFromAPI,
8+
} from '$lib/utilities/request-from-api';
39
import { routeForApi } from '$lib/utilities/route-for-api';
410
import { toSearchAttributeTypeReadable } from '$lib/utilities/screaming-enums';
511

612
export const fetchSearchAttributesForNamespace = async (
713
namespace: string,
814
request = fetch,
9-
): Promise<Omit<SearchAttributesResponse, 'storageSchema'>> => {
15+
): Promise<SearchAttributesResponseHumanized> => {
1016
try {
11-
const route = routeForApi('search-attributes', { namespace });
12-
const searchAttributesResponse =
13-
await requestFromAPI<SearchAttributesResponse>(route, {
14-
request,
15-
});
16-
17-
const customAttributes = { ...searchAttributesResponse.customAttributes };
18-
const systemAttributes = { ...searchAttributesResponse.systemAttributes };
19-
Object.entries(customAttributes).forEach(([key, value]) => {
20-
customAttributes[key] = toSearchAttributeTypeReadable(value);
21-
});
22-
Object.entries(systemAttributes).forEach(([key, value]) => {
23-
systemAttributes[key] = toSearchAttributeTypeReadable(value);
24-
});
25-
return {
26-
customAttributes,
27-
systemAttributes,
28-
};
29-
} catch (e) {
30-
console.error(
31-
'Error fetching search attributes for namespace',
32-
namespace,
33-
e,
34-
);
35-
return {
36-
customAttributes: {},
37-
systemAttributes: {},
38-
};
17+
// get the search attributes from the API
18+
const parsed = await parsedResponse({ namespace, request });
19+
20+
// parse the response to a human readable format
21+
return mutateSearchAttributes(parsed);
22+
} catch (err) {
23+
// handle the error and return a default value
24+
handleError({ namespace, err });
3925
}
4026
};
27+
28+
async function parsedResponse({
29+
namespace,
30+
request = fetch,
31+
}: {
32+
namespace: string;
33+
request?: typeof fetch;
34+
}) {
35+
const route = routeForApi('search-attributes', { namespace });
36+
const searchAttributesResponse =
37+
await requestFromAPI<ListSearchAttributesResponse>(route, {
38+
request,
39+
});
40+
41+
// The parse call is integral to the service implementation as it passes the
42+
// response through the zod schema and validates the content
43+
return ListSearchAttributesResponse.parse(searchAttributesResponse);
44+
}
45+
46+
function mutateSearchAttributes(
47+
parsed: ListSearchAttributesResponse,
48+
): SearchAttributesResponseHumanized {
49+
return {
50+
customAttributes: mapEntries(
51+
parsed.customAttributes || {},
52+
toSearchAttributeTypeReadable,
53+
),
54+
55+
systemAttributes: mapEntries(
56+
parsed.systemAttributes || {},
57+
toSearchAttributeTypeReadable,
58+
),
59+
};
60+
}
61+
62+
function handleError({
63+
namespace,
64+
err,
65+
}: {
66+
namespace: string;
67+
err: unknown;
68+
}): SearchAttributesResponseHumanized {
69+
let message = 'Error fetching search attributes';
70+
if (isTemporalAPIError(err)) {
71+
message = 'Error fetching search attributes for namespace:';
72+
}
73+
74+
console.error(message, namespace, err);
75+
76+
return {
77+
customAttributes: {},
78+
systemAttributes: {},
79+
};
80+
}

src/lib/types/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,6 @@ export type ScheduleActionResult =
218218
// api.query
219219
export type QueryResult = temporal.api.query.v1.IWorkflowQueryResult;
220220

221-
// api.operatorservice
222-
export type ListSearchAttributesResponse =
223-
temporal.api.operatorservice.v1.IListSearchAttributesResponse;
224-
225221
// api.batch
226222
export type BatchCancelOperation =
227223
temporal.api.batch.v1.IBatchOperationCancellation;

src/lib/types/workflows.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,9 @@ export type SearchAttributes = {
136136
[k: string]: SearchAttributeType;
137137
};
138138

139-
export type SearchAttributesResponse = {
139+
export type SearchAttributesResponseHumanized = {
140140
customAttributes: Record<string, SearchAttributeType>;
141141
systemAttributes: Record<string, SearchAttributeType>;
142-
storageSchema: import('$lib/types').ListSearchAttributesResponse['storageSchema'];
143142
};
144143

145144
export type WorkflowSearchAttributes = {

src/lib/utilities/map-entries.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* This function is probably a bit obtuse, so lets break it down:
3+
*
4+
1. **Generic Function**:
5+
- The function `mapEntries` is a generic function, which means it can work
6+
with any data types specified at the time of calling. It uses four generic
7+
type parameters: `T`, `U`, `V`, and `W`.
8+
9+
2. **Type Parameters**:
10+
- `T`: Represents the type of the values in the input object `obj`.
11+
- `U`: Represents the type of the values in the output object.
12+
- `V`: A record type where keys are strings and values are of type `T`.
13+
- `W`: A record type where keys are strings and values are of type `U`.
14+
15+
3. **Function Parameters**:
16+
- `obj: V`: The input object, which is a record with string keys and values
17+
of type `T`.
18+
- `fn: (value: T) => U`: A function that takes a value of type `T` and
19+
returns a value of type `U`.
20+
21+
4. **Function Logic**:
22+
- `Object.entries(obj)`: Converts the input object `obj` into an array of
23+
key-value pairs.
24+
- `.map(([key, value]) => [key, fn(value)])`: Iterates over each key-value
25+
pair, applying the function `fn` to each value. It returns a new array of
26+
key-value pairs where the values have been transformed by `fn`.
27+
- `Object.fromEntries(...)`: Converts the array of transformed key-value
28+
pairs back into an object.
29+
30+
5. **Return Type**:
31+
- The function returns an object of type `W`, which is a record with the
32+
same keys as the input object but with values transformed to type `U`.
33+
34+
### Example
35+
36+
Suppose you have an object representing search attributes in a workflow, and
37+
you want to convert the property values from screaming enums to humanized names.
38+
Here's how you might use `mapEntries` to achieve this:
39+
40+
#### Input
41+
42+
Suppose you have an object representing search attributes with screaming enum
43+
values:
44+
45+
```typescript
46+
const searchAttributes = {
47+
attribute1: 'INDEXED_VALUE_TYPE_STRING',
48+
attribute2: 'INDEXED_VALUE_TYPE_INT',
49+
attribute3: 'INDEXED_VALUE_TYPE_BOOL',
50+
};
51+
```
52+
53+
#### Transformation Function
54+
55+
We'll use the `toSearchAttributeTypeReadable` function to convert these
56+
screaming enum values into a more readable format:
57+
58+
```typescript
59+
import { toSearchAttributeTypeReadable } from '$lib/utilities/screaming-enums';
60+
61+
const humanizedAttributes = mapEntries(searchAttributes, toSearchAttributeTypeReadable);
62+
```
63+
64+
#### Output
65+
66+
The `humanizedAttributes` object will be:
67+
68+
```typescript
69+
{
70+
attribute1: 'String',
71+
attribute2: 'Int',
72+
attribute3: 'Bool',
73+
}
74+
```
75+
76+
### Explanation
77+
78+
- **Input Object**: `searchAttributes` is an object where each key is an
79+
attribute name and each value is a screaming enum representing the attribute
80+
type.
81+
- **Transformation Function**: `toSearchAttributeTypeReadable` converts each
82+
screaming enum value into a human-readable string by removing the prefix and
83+
capitalizing the first letter of each word.
84+
- **Output Object**: `humanizedAttributes` is the result of applying
85+
`mapEntries` to `searchAttributes` with `toSearchAttributeTypeReadable`. It has
86+
the same keys as `searchAttributes`, but the values are transformed to a more
87+
readable format.
88+
*/
89+
export function mapEntries<
90+
T,
91+
U,
92+
V extends Record<string, T>,
93+
W extends Record<string, U>,
94+
>(obj: V, fn: (value: T) => U): W {
95+
return Object.fromEntries(
96+
Object.entries(obj).map(([key, value]) => [key, fn(value)]),
97+
) as W;
98+
}

src/lib/utilities/screaming-enums.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ListSearchAttributesResponse } from '@temporalio/schemas';
2+
13
import type {
24
ArchivalState,
35
CallbackState,
@@ -14,10 +16,10 @@ import type {
1416

1517
import type { EventType } from './is-event-type';
1618

17-
export const fromScreamingEnum = <T>(
18-
potentialScreamingEnum: T,
19+
export const fromScreamingEnum = <T, V>(
20+
potentialScreamingEnum: V,
1921
prefix: string,
20-
): T => {
22+
): T | V => {
2123
if (!potentialScreamingEnum) return potentialScreamingEnum;
2224
const stringEnum = String(potentialScreamingEnum);
2325
const split = stringEnum.split('_');
@@ -31,8 +33,11 @@ export const fromScreamingEnum = <T>(
3133
};
3234

3335
export const toSearchAttributeTypeReadable = (
34-
status: SearchAttributeType,
35-
): SearchAttributeType => {
36+
status:
37+
| ListSearchAttributesResponse['customAttributes'][string]
38+
| ListSearchAttributesResponse['systemAttributes'][string]
39+
| SearchAttributeType,
40+
): typeof status => {
3641
return fromScreamingEnum(status, 'IndexedValueType');
3742
};
3843

0 commit comments

Comments
 (0)