Skip to content

Commit 9f14fb9

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 9530667 commit 9f14fb9

File tree

5 files changed

+170
-41
lines changed

5 files changed

+170
-41
lines changed

Diff for: src/lib/services/search-attributes-service.ts

+62-30
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,72 @@
1-
import type { SearchAttributesResponse } from '$lib/types/workflows';
1+
import { ListSearchAttributesResponse } from '$lib/schemas';
2+
import type { SearchAttributesResponseHumanized } from '$lib/types/workflows';
3+
import { mapEntries } from '$lib/utilities/map-entries';
24
import { requestFromAPI } from '$lib/utilities/request-from-api';
35
import { routeForApi } from '$lib/utilities/route-for-api';
46
import { toSearchAttributeTypeReadable } from '$lib/utilities/screaming-enums';
57

68
export const fetchSearchAttributesForNamespace = async (
79
namespace: string,
810
request = fetch,
9-
): Promise<Omit<SearchAttributesResponse, 'storageSchema'>> => {
11+
): Promise<SearchAttributesResponseHumanized> => {
1012
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-
};
13+
// get the search attributes from the API
14+
const parsed = await parsedResponse({ namespace, request });
15+
16+
// parse the response to a human readable format
17+
return humanizeSearchAttributes(parsed);
18+
} catch (err) {
19+
// handle the error and return a default value
20+
handleError({ namespace, err });
3921
}
4022
};
23+
24+
async function parsedResponse({
25+
namespace,
26+
request = fetch,
27+
}: {
28+
namespace: string;
29+
request?: typeof fetch;
30+
}) {
31+
const route = routeForApi('search-attributes', { namespace });
32+
const searchAttributesResponse =
33+
await requestFromAPI<ListSearchAttributesResponse>(route, {
34+
request,
35+
});
36+
37+
// The parse call is integral to the service implementation as it passes the
38+
// response through the zod schema and validates the content
39+
return ListSearchAttributesResponse.parse(searchAttributesResponse);
40+
}
41+
42+
function humanizeSearchAttributes(
43+
parsed: ListSearchAttributesResponse,
44+
): SearchAttributesResponseHumanized {
45+
return {
46+
customAttributes: mapEntries(
47+
parsed.customAttributes || {},
48+
toSearchAttributeTypeReadable,
49+
),
50+
51+
systemAttributes: mapEntries(
52+
parsed.systemAttributes || {},
53+
toSearchAttributeTypeReadable,
54+
),
55+
};
56+
}
57+
58+
function handleError({
59+
namespace,
60+
err,
61+
}: {
62+
namespace: string;
63+
err: unknown;
64+
}): SearchAttributesResponseHumanized {
65+
const message = 'Error fetching search attributes for namespace:';
66+
console.error(message, namespace, err);
67+
68+
return {
69+
customAttributes: {},
70+
systemAttributes: {},
71+
};
72+
}

Diff for: src/lib/types/index.ts

-4
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;

Diff for: src/lib/types/workflows.ts

+1-2
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 = {

Diff for: src/lib/utilities/map-entries.ts

+98
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+
}

Diff for: src/lib/utilities/screaming-enums.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ListSearchAttributesResponse } from '$lib/schemas';
12
import type {
23
ArchivalState,
34
CallbackState,
@@ -14,10 +15,10 @@ import type {
1415

1516
import type { EventType } from './is-event-type';
1617

17-
export const fromScreamingEnum = <T>(
18-
potentialScreamingEnum: T,
18+
export const fromScreamingEnum = <T, V>(
19+
potentialScreamingEnum: V,
1920
prefix: string,
20-
): T => {
21+
): T | V => {
2122
if (!potentialScreamingEnum) return potentialScreamingEnum;
2223
const stringEnum = String(potentialScreamingEnum);
2324
const split = stringEnum.split('_');
@@ -31,8 +32,11 @@ export const fromScreamingEnum = <T>(
3132
};
3233

3334
export const toSearchAttributeTypeReadable = (
34-
status: SearchAttributeType,
35-
): SearchAttributeType => {
35+
status:
36+
| ListSearchAttributesResponse['customAttributes'][string]
37+
| ListSearchAttributesResponse['systemAttributes'][string]
38+
| SearchAttributeType,
39+
): typeof status => {
3640
return fromScreamingEnum(status, 'IndexedValueType');
3741
};
3842

0 commit comments

Comments
 (0)