Skip to content

Commit d5ce13c

Browse files
Add new failover history list endpoint (cadence-workflow#1061)
Add new route handler for a domain, to list failover history using the ListFailoverHistory endpoint. Signed-off-by: Adhitya Mamallan <adhitya.mamallan@uber.com>
1 parent f081c74 commit d5ce13c

File tree

7 files changed

+293
-0
lines changed

7 files changed

+293
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type NextRequest } from 'next/server';
2+
3+
import { listFailoverHistory } from '@/route-handlers/list-failover-history/list-failover-history';
4+
import { type RouteParams } from '@/route-handlers/list-failover-history/list-failover-history.types';
5+
import { routeHandlerWithMiddlewares } from '@/utils/route-handlers-middleware';
6+
import routeHandlersDefaultMiddlewares from '@/utils/route-handlers-middleware/config/route-handlers-default-middlewares.config';
7+
8+
export async function GET(
9+
request: NextRequest,
10+
options: { params: RouteParams }
11+
) {
12+
return routeHandlerWithMiddlewares(
13+
listFailoverHistory,
14+
request,
15+
options,
16+
routeHandlersDefaultMiddlewares
17+
);
18+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { NextRequest } from 'next/server';
2+
3+
import type { ListFailoverHistoryResponse as OriginalListFailoverHistoryResponse } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListFailoverHistoryResponse';
4+
import { GRPCError } from '@/utils/grpc/grpc-error';
5+
import { mockGrpcClusterMethods } from '@/utils/route-handlers-middleware/middlewares/__mocks__/grpc-cluster-methods';
6+
7+
import { listFailoverHistory } from '../list-failover-history';
8+
import {
9+
type Context,
10+
type RequestParams,
11+
} from '../list-failover-history.types';
12+
13+
beforeEach(() => {
14+
jest.resetAllMocks();
15+
});
16+
17+
describe(listFailoverHistory.name, () => {
18+
it('calls listFailoverHistory with correct parameters and returns valid response', async () => {
19+
const { res, mockListFailoverHistory, mockSuccessResponse } = await setup({
20+
domainId: 'test-domain-id',
21+
});
22+
23+
expect(mockListFailoverHistory).toHaveBeenCalledWith({
24+
filters: {
25+
domainId: 'test-domain-id',
26+
},
27+
pagination: {
28+
nextPageToken: undefined,
29+
},
30+
});
31+
const responseJson = await res.json();
32+
expect(responseJson).toEqual(mockSuccessResponse);
33+
expect(res.status).toEqual(200);
34+
});
35+
36+
it('calls listFailoverHistory with nextPage token when provided', async () => {
37+
const { res, mockListFailoverHistory } = await setup({
38+
domainId: 'test-domain-id',
39+
nextPage: 'token-123',
40+
});
41+
42+
expect(mockListFailoverHistory).toHaveBeenCalledWith({
43+
filters: {
44+
domainId: 'test-domain-id',
45+
},
46+
pagination: {
47+
nextPageToken: 'token-123',
48+
},
49+
});
50+
const responseJson = await res.json();
51+
expect(responseJson.nextPageToken).toEqual('next-token-456');
52+
expect(res.status).toEqual(200);
53+
});
54+
55+
it('returns 400 when query parameters are invalid', async () => {
56+
const { res } = await setup({
57+
skipDomainId: true,
58+
});
59+
60+
expect(res.status).toEqual(400);
61+
const responseJson = await res.json();
62+
expect(responseJson).toEqual(
63+
expect.objectContaining({
64+
message: 'Invalid argument(s) for fetching failover history',
65+
validationErrors: expect.arrayContaining([
66+
expect.objectContaining({
67+
code: 'invalid_type',
68+
path: ['domainId'],
69+
}),
70+
]),
71+
})
72+
);
73+
});
74+
75+
it('returns an error when listFailoverHistory errors out with GRPCError', async () => {
76+
const { res, mockListFailoverHistory } = await setup({
77+
domainId: 'test-domain-id',
78+
error: new GRPCError('Failed to fetch failover history'),
79+
});
80+
81+
expect(mockListFailoverHistory).toHaveBeenCalled();
82+
expect(res.status).toEqual(500);
83+
const responseJson = await res.json();
84+
expect(responseJson).toEqual(
85+
expect.objectContaining({
86+
message: 'Failed to fetch failover history',
87+
})
88+
);
89+
});
90+
91+
it('returns an error when listFailoverHistory errors out with generic error', async () => {
92+
const { res, mockListFailoverHistory } = await setup({
93+
domainId: 'test-domain-id',
94+
error: new Error('Network error'),
95+
});
96+
97+
expect(mockListFailoverHistory).toHaveBeenCalled();
98+
expect(res.status).toEqual(500);
99+
const responseJson = await res.json();
100+
expect(responseJson).toEqual(
101+
expect.objectContaining({
102+
message: 'Error fetching failover history',
103+
})
104+
);
105+
});
106+
});
107+
108+
async function setup({
109+
domainId,
110+
nextPage,
111+
error,
112+
skipDomainId,
113+
}: {
114+
domainId?: string;
115+
nextPage?: string;
116+
error?: Error;
117+
skipDomainId?: boolean;
118+
}) {
119+
const mockSuccessResponse: OriginalListFailoverHistoryResponse = {
120+
failoverEvents: [],
121+
nextPageToken: nextPage ? 'next-token-456' : '',
122+
};
123+
124+
const mockListFailoverHistory = jest
125+
.spyOn(mockGrpcClusterMethods, 'listFailoverHistory')
126+
.mockImplementationOnce(async () => {
127+
if (error) {
128+
throw error;
129+
}
130+
return mockSuccessResponse;
131+
});
132+
133+
const queryParams = new URLSearchParams();
134+
if (!skipDomainId && domainId !== undefined) {
135+
queryParams.append('domainId', domainId);
136+
}
137+
if (nextPage !== undefined) {
138+
queryParams.append('nextPage', nextPage);
139+
}
140+
141+
const url = `http://localhost/api/clusters/test-cluster/domains/test-domain/failover-history?${queryParams.toString()}`;
142+
143+
const res = await listFailoverHistory(
144+
new NextRequest(url, {
145+
method: 'GET',
146+
}),
147+
{
148+
params: {
149+
domain: 'test-domain',
150+
cluster: 'test-cluster',
151+
},
152+
} as RequestParams,
153+
{
154+
grpcClusterMethods: mockGrpcClusterMethods,
155+
} as Context
156+
);
157+
158+
return { res, mockListFailoverHistory, mockSuccessResponse };
159+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { type NextRequest, NextResponse } from 'next/server';
2+
import queryString from 'query-string';
3+
4+
import decodeUrlParams from '@/utils/decode-url-params';
5+
import { getHTTPStatusCode, GRPCError } from '@/utils/grpc/grpc-error';
6+
import logger, { type RouteHandlerErrorPayload } from '@/utils/logger';
7+
8+
import type {
9+
ListFailoverHistoryResponse,
10+
Context,
11+
RequestParams,
12+
RouteParams,
13+
} from './list-failover-history.types';
14+
import listFailoverHistoryQueryParamsSchema from './schemas/list-failover-history-query-params-schema';
15+
16+
export async function listFailoverHistory(
17+
request: NextRequest,
18+
requestParams: RequestParams,
19+
ctx: Context
20+
) {
21+
const decodedParams = decodeUrlParams(requestParams.params) as RouteParams;
22+
23+
const { data: queryParams, error } =
24+
listFailoverHistoryQueryParamsSchema.safeParse(
25+
queryString.parse(request.nextUrl.searchParams.toString())
26+
);
27+
28+
if (error) {
29+
return NextResponse.json(
30+
{
31+
message: 'Invalid argument(s) for fetching failover history',
32+
validationErrors: error.errors,
33+
},
34+
{
35+
status: 400,
36+
}
37+
);
38+
}
39+
40+
try {
41+
const response: ListFailoverHistoryResponse =
42+
await ctx.grpcClusterMethods.listFailoverHistory({
43+
filters: {
44+
domainId: queryParams.domainId,
45+
},
46+
pagination: {
47+
nextPageToken: queryParams.nextPage,
48+
},
49+
});
50+
51+
return NextResponse.json(response);
52+
} catch (e) {
53+
logger.error<RouteHandlerErrorPayload>(
54+
{ requestParams: decodedParams, queryParams, error: e },
55+
'Error fetching failover history' +
56+
(e instanceof GRPCError ? ': ' + e.message : '')
57+
);
58+
59+
return NextResponse.json(
60+
{
61+
message:
62+
e instanceof GRPCError
63+
? e.message
64+
: 'Error fetching failover history',
65+
cause: e,
66+
},
67+
{
68+
status: getHTTPStatusCode(e),
69+
}
70+
);
71+
}
72+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type z } from 'zod';
2+
3+
import { type ListFailoverHistoryResponse as ListFailoverHistoryResponseProto } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListFailoverHistoryResponse';
4+
import { type DefaultMiddlewaresContext } from '@/utils/route-handlers-middleware';
5+
6+
import type listFailoverHistoryQueryParamsSchema from './schemas/list-failover-history-query-params-schema';
7+
8+
export type RouteParams = {
9+
domain: string;
10+
cluster: string;
11+
};
12+
13+
export type RequestParams = {
14+
params: RouteParams;
15+
};
16+
17+
export type ListFailoverHistoryRequestQueryParams = z.input<
18+
typeof listFailoverHistoryQueryParamsSchema
19+
>;
20+
21+
export type ListFailoverHistoryResponse = ListFailoverHistoryResponseProto;
22+
23+
export type Context = DefaultMiddlewaresContext;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { z } from 'zod';
2+
3+
const listFailoverHistoryQueryParamsSchema = z.object({
4+
domainId: z.string(),
5+
nextPage: z.string().optional(),
6+
});
7+
8+
export default listFailoverHistoryQueryParamsSchema;

src/utils/grpc/grpc-client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { type ListClosedWorkflowExecutionsRequest__Input } from '@/__generated__
1818
import { type ListClosedWorkflowExecutionsResponse } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListClosedWorkflowExecutionsResponse';
1919
import { type ListDomainsRequest__Input } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListDomainsRequest';
2020
import { type ListDomainsResponse } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListDomainsResponse';
21+
import { type ListFailoverHistoryRequest__Input } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListFailoverHistoryRequest';
22+
import { type ListFailoverHistoryResponse } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListFailoverHistoryResponse';
2123
import { type ListOpenWorkflowExecutionsRequest__Input } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListOpenWorkflowExecutionsRequest';
2224
import { type ListOpenWorkflowExecutionsResponse } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListOpenWorkflowExecutionsResponse';
2325
import { type ListTaskListPartitionsRequest__Input } from '@/__generated__/proto-ts/uber/cadence/api/v1/ListTaskListPartitionsRequest';
@@ -122,6 +124,9 @@ export type GRPCClusterMethods = {
122124
getDiagnosticsWorkflow: (
123125
payload: DiagnoseWorkflowExecutionRequest__Input
124126
) => Promise<DiagnoseWorkflowExecutionResponse>;
127+
listFailoverHistory: (
128+
payload: ListFailoverHistoryRequest__Input
129+
) => Promise<ListFailoverHistoryResponse>;
125130
};
126131

127132
// cache services instances
@@ -343,6 +348,13 @@ const getClusterServicesMethods = async (
343348
method: 'DiagnoseWorkflowExecution',
344349
metadata: metadata,
345350
}),
351+
listFailoverHistory: domainService.request<
352+
ListFailoverHistoryRequest__Input,
353+
ListFailoverHistoryResponse
354+
>({
355+
method: 'ListFailoverHistory',
356+
metadata: metadata,
357+
}),
346358
};
347359
};
348360

src/utils/route-handlers-middleware/middlewares/__mocks__/grpc-cluster-methods.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const mockGrpcClusterMethods: GRPCClusterMethods = {
1313
getDiagnosticsWorkflow: jest.fn(),
1414
getSearchAttributes: jest.fn(),
1515
listDomains: jest.fn(),
16+
listFailoverHistory: jest.fn(),
1617
listTaskListPartitions: jest.fn(),
1718
listWorkflows: jest.fn(),
1819
openWorkflows: jest.fn(),

0 commit comments

Comments
 (0)