Skip to content

Commit a90897d

Browse files
authored
[Issue 2448] use v1 search endpoint and frontend fetch pattern refactors (#2518)
* opportunity search change to use the `v1` version of the endpoint and activate the use of Open Search on the backend * various refactors to the `API` fetch system for general improvement and some minor bug fixes * fix to loading spinner test that was acting flaky * update e2e script to include seeding open search
1 parent a0bc389 commit a90897d

15 files changed

+433
-313
lines changed

.github/workflows/ci-frontend-e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- name: Start API Server for e2e tests
4444
run: |
4545
cd ../api
46-
make init db-seed-local start &
46+
make init db-seed-local populate-search-opportunities start &
4747
cd ../frontend
4848
# Ensure the API wait script is executable
4949
chmod +x ../api/bin/wait-for-api.sh

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"scripts": {
99
"build": "next build",
1010
"dev": "next dev",
11+
"debug": "NODE_OPTIONS='--inspect' next dev",
1112
"format": "prettier --write '**/*.{js,json,md,mdx,ts,tsx,scss,yaml,yml}'",
1213
"format-check": "prettier --check '**/*.{js,json,md,mdx,ts,tsx,scss,yaml,yml}'",
1314
"lint": "next lint --dir src --dir stories --dir .storybook --dir tests --dir scripts --dir frontend --dir lib --dir types",

frontend/src/app/api/BaseApi.ts

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,26 @@ import {
1515
ValidationError,
1616
} from "src/errors";
1717
import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher";
18-
// TODO (#1682): replace search specific references (since this is a generic API file that any
19-
// future page or different namespace could use)
2018
import { APIResponse } from "src/types/apiResponseTypes";
2119

2220
export type ApiMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
2321
export interface JSONRequestBody {
2422
[key: string]: unknown;
2523
}
2624

27-
interface APIResponseError {
28-
field: string;
29-
message: string;
30-
type: string;
31-
}
32-
3325
export interface HeadersDict {
3426
[header: string]: string;
3527
}
3628

3729
export default abstract class BaseApi {
38-
// Root path of API resource without leading slash.
39-
abstract get basePath(): string;
30+
// Root path of API resource without leading slash, can be overridden by implementing API classes as necessary
31+
get basePath() {
32+
return environment.API_URL;
33+
}
4034

41-
// API version
35+
// API version, can be overridden by implementing API classes as necessary
4236
get version() {
43-
return "v0.1";
37+
return "v1";
4438
}
4539

4640
// Namespace representing the API resource
@@ -54,29 +48,28 @@ export default abstract class BaseApi {
5448
if (environment.API_AUTH_TOKEN) {
5549
headers["X-AUTH"] = environment.API_AUTH_TOKEN;
5650
}
51+
headers["Content-Type"] = "application/json";
5752
return headers;
5853
}
5954

6055
/**
6156
* Send an API request.
6257
*/
63-
async request(
58+
async request<ResponseType extends APIResponse>(
6459
method: ApiMethod,
65-
basePath: string,
66-
namespace: string,
6760
subPath: string,
6861
queryParamData?: QueryParamData,
6962
body?: JSONRequestBody,
7063
options: {
7164
additionalHeaders?: HeadersDict;
7265
} = {},
73-
) {
66+
): Promise<ResponseType> {
7467
const { additionalHeaders = {} } = options;
7568
const url = createRequestUrl(
7669
method,
77-
basePath,
70+
this.basePath,
7871
this.version,
79-
namespace,
72+
this.namespace,
8073
subPath,
8174
body,
8275
);
@@ -85,8 +78,7 @@ export default abstract class BaseApi {
8578
...this.headers,
8679
};
8780

88-
headers["Content-Type"] = "application/json";
89-
const response = await this.sendRequest(
81+
const response = await this.sendRequest<ResponseType>(
9082
url,
9183
{
9284
body: method === "GET" || !body ? null : createRequestBody(body),
@@ -102,16 +94,16 @@ export default abstract class BaseApi {
10294
/**
10395
* Send a request and handle the response
10496
*/
105-
private async sendRequest(
97+
private async sendRequest<ResponseType extends APIResponse>(
10698
url: string,
10799
fetchOptions: RequestInit,
108100
queryParamData?: QueryParamData,
109-
) {
110-
let response: Response;
111-
let responseBody: APIResponse;
101+
): Promise<ResponseType> {
102+
let response;
103+
let responseBody;
112104
try {
113105
response = await fetch(url, fetchOptions);
114-
responseBody = (await response.json()) as APIResponse;
106+
responseBody = (await response.json()) as ResponseType;
115107
} catch (error) {
116108
// API most likely down, but also possibly an error setting up or sending a request
117109
// or parsing the response.
@@ -121,16 +113,7 @@ export default abstract class BaseApi {
121113
handleNotOkResponse(responseBody, url, queryParamData);
122114
}
123115

124-
const { data, message, pagination_info, status_code, warnings } =
125-
responseBody;
126-
127-
return {
128-
data,
129-
message,
130-
pagination_info,
131-
status_code,
132-
warnings,
133-
};
116+
return responseBody;
134117
}
135118
}
136119

@@ -149,14 +132,14 @@ export function createRequestUrl(
149132
let url = [...cleanedPaths].join("/");
150133
if (method === "GET" && body && !(body instanceof FormData)) {
151134
// Append query string to URL
152-
const body: { [key: string]: string } = {};
135+
const newBody: { [key: string]: string } = {};
153136
Object.entries(body).forEach(([key, value]) => {
154137
const stringValue =
155138
typeof value === "string" ? value : JSON.stringify(value);
156-
body[key] = stringValue;
139+
newBody[key] = stringValue;
157140
});
158141

159-
const params = new URLSearchParams(body).toString();
142+
const params = new URLSearchParams(newBody).toString();
160143
url = `${url}?${params}`;
161144
}
162145
return url;
@@ -206,7 +189,7 @@ function handleNotOkResponse(
206189
throwError(response, url, searchInputs);
207190
} else {
208191
if (errors) {
209-
const firstError = errors[0] as APIResponseError;
192+
const firstError = errors[0];
210193
throwError(response, url, searchInputs, firstError);
211194
}
212195
}
@@ -216,9 +199,9 @@ const throwError = (
216199
response: APIResponse,
217200
url: string,
218201
searchInputs?: QueryParamData,
219-
firstError?: APIResponseError,
202+
firstError?: unknown,
220203
) => {
221-
const { status_code, message } = response;
204+
const { status_code = 0, message = "" } = response;
222205
console.error(
223206
`API request error at ${url} (${status_code}): ${message}`,
224207
searchInputs,
Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,21 @@
11
import "server-only";
22

3-
import { environment } from "src/constants/environments";
43
import { OpportunityApiResponse } from "src/types/opportunity/opportunityResponseTypes";
54

65
import BaseApi from "./BaseApi";
76

87
export default class OpportunityListingAPI extends BaseApi {
9-
get version(): string {
10-
return "v1";
11-
}
12-
13-
get basePath(): string {
14-
return environment.API_URL;
15-
}
16-
178
get namespace(): string {
189
return "opportunities";
1910
}
2011

2112
async getOpportunityById(
2213
opportunityId: number,
2314
): Promise<OpportunityApiResponse> {
24-
const subPath = `${opportunityId}`;
25-
const response = (await this.request(
15+
const response = await this.request<OpportunityApiResponse>(
2616
"GET",
27-
this.basePath,
28-
this.namespace,
29-
subPath,
30-
)) as OpportunityApiResponse;
17+
`${opportunityId}`,
18+
);
3119
return response;
3220
}
3321
}

0 commit comments

Comments
 (0)