Skip to content

Commit

Permalink
[Issue 2448] use v1 search endpoint and frontend fetch pattern refact…
Browse files Browse the repository at this point in the history
…ors (#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
  • Loading branch information
doug-s-nava authored Oct 21, 2024
1 parent a0bc389 commit a90897d
Show file tree
Hide file tree
Showing 15 changed files with 433 additions and 313 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-frontend-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Start API Server for e2e tests
run: |
cd ../api
make init db-seed-local start &
make init db-seed-local populate-search-opportunities start &
cd ../frontend
# Ensure the API wait script is executable
chmod +x ../api/bin/wait-for-api.sh
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"scripts": {
"build": "next build",
"dev": "next dev",
"debug": "NODE_OPTIONS='--inspect' next dev",
"format": "prettier --write '**/*.{js,json,md,mdx,ts,tsx,scss,yaml,yml}'",
"format-check": "prettier --check '**/*.{js,json,md,mdx,ts,tsx,scss,yaml,yml}'",
"lint": "next lint --dir src --dir stories --dir .storybook --dir tests --dir scripts --dir frontend --dir lib --dir types",
Expand Down
65 changes: 24 additions & 41 deletions frontend/src/app/api/BaseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,26 @@ import {
ValidationError,
} from "src/errors";
import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher";
// TODO (#1682): replace search specific references (since this is a generic API file that any
// future page or different namespace could use)
import { APIResponse } from "src/types/apiResponseTypes";

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

interface APIResponseError {
field: string;
message: string;
type: string;
}

export interface HeadersDict {
[header: string]: string;
}

export default abstract class BaseApi {
// Root path of API resource without leading slash.
abstract get basePath(): string;
// Root path of API resource without leading slash, can be overridden by implementing API classes as necessary
get basePath() {
return environment.API_URL;
}

// API version
// API version, can be overridden by implementing API classes as necessary
get version() {
return "v0.1";
return "v1";
}

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

/**
* Send an API request.
*/
async request(
async request<ResponseType extends APIResponse>(
method: ApiMethod,
basePath: string,
namespace: string,
subPath: string,
queryParamData?: QueryParamData,
body?: JSONRequestBody,
options: {
additionalHeaders?: HeadersDict;
} = {},
) {
): Promise<ResponseType> {
const { additionalHeaders = {} } = options;
const url = createRequestUrl(
method,
basePath,
this.basePath,
this.version,
namespace,
this.namespace,
subPath,
body,
);
Expand All @@ -85,8 +78,7 @@ export default abstract class BaseApi {
...this.headers,
};

headers["Content-Type"] = "application/json";
const response = await this.sendRequest(
const response = await this.sendRequest<ResponseType>(
url,
{
body: method === "GET" || !body ? null : createRequestBody(body),
Expand All @@ -102,16 +94,16 @@ export default abstract class BaseApi {
/**
* Send a request and handle the response
*/
private async sendRequest(
private async sendRequest<ResponseType extends APIResponse>(
url: string,
fetchOptions: RequestInit,
queryParamData?: QueryParamData,
) {
let response: Response;
let responseBody: APIResponse;
): Promise<ResponseType> {
let response;
let responseBody;
try {
response = await fetch(url, fetchOptions);
responseBody = (await response.json()) as APIResponse;
responseBody = (await response.json()) as ResponseType;
} catch (error) {
// API most likely down, but also possibly an error setting up or sending a request
// or parsing the response.
Expand All @@ -121,16 +113,7 @@ export default abstract class BaseApi {
handleNotOkResponse(responseBody, url, queryParamData);
}

const { data, message, pagination_info, status_code, warnings } =
responseBody;

return {
data,
message,
pagination_info,
status_code,
warnings,
};
return responseBody;
}
}

Expand All @@ -149,14 +132,14 @@ export function createRequestUrl(
let url = [...cleanedPaths].join("/");
if (method === "GET" && body && !(body instanceof FormData)) {
// Append query string to URL
const body: { [key: string]: string } = {};
const newBody: { [key: string]: string } = {};
Object.entries(body).forEach(([key, value]) => {
const stringValue =
typeof value === "string" ? value : JSON.stringify(value);
body[key] = stringValue;
newBody[key] = stringValue;
});

const params = new URLSearchParams(body).toString();
const params = new URLSearchParams(newBody).toString();
url = `${url}?${params}`;
}
return url;
Expand Down Expand Up @@ -206,7 +189,7 @@ function handleNotOkResponse(
throwError(response, url, searchInputs);
} else {
if (errors) {
const firstError = errors[0] as APIResponseError;
const firstError = errors[0];
throwError(response, url, searchInputs, firstError);
}
}
Expand All @@ -216,9 +199,9 @@ const throwError = (
response: APIResponse,
url: string,
searchInputs?: QueryParamData,
firstError?: APIResponseError,
firstError?: unknown,
) => {
const { status_code, message } = response;
const { status_code = 0, message = "" } = response;
console.error(
`API request error at ${url} (${status_code}): ${message}`,
searchInputs,
Expand Down
18 changes: 3 additions & 15 deletions frontend/src/app/api/OpportunityListingAPI.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import "server-only";

import { environment } from "src/constants/environments";
import { OpportunityApiResponse } from "src/types/opportunity/opportunityResponseTypes";

import BaseApi from "./BaseApi";

export default class OpportunityListingAPI extends BaseApi {
get version(): string {
return "v1";
}

get basePath(): string {
return environment.API_URL;
}

get namespace(): string {
return "opportunities";
}

async getOpportunityById(
opportunityId: number,
): Promise<OpportunityApiResponse> {
const subPath = `${opportunityId}`;
const response = (await this.request(
const response = await this.request<OpportunityApiResponse>(
"GET",
this.basePath,
this.namespace,
subPath,
)) as OpportunityApiResponse;
`${opportunityId}`,
);
return response;
}
}
Loading

0 comments on commit a90897d

Please sign in to comment.