From a3376a8cc1990084f6c2788c9ca66e873712a27a Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Thu, 22 Aug 2024 08:00:10 -0500 Subject: [PATCH] Updated all api calls to use new apiCallFactory. --- src/api/addStar.ts | 26 +++---- src/api/apiCallFactory.ts | 102 ++++++++++++++++++++------- src/api/getImage.ts | 33 ++++----- src/api/getRecipe.ts | 56 +++++++-------- src/api/getRecipeInfos.ts | 4 +- src/api/removeStar.ts | 26 +++---- src/api/updateRecipe.ts | 40 ++++------- src/pages/edit-recipe/EditRecipe.tsx | 10 ++- src/pages/recipe/Recipe.tsx | 21 ++++-- src/pages/recipes/Recipes.tsx | 1 + 10 files changed, 173 insertions(+), 146 deletions(-) diff --git a/src/api/addStar.ts b/src/api/addStar.ts index 12100e5..672101e 100644 --- a/src/api/addStar.ts +++ b/src/api/addStar.ts @@ -1,27 +1,21 @@ import AccessToken from '../types/AccessToken' -import { ApiError } from './ApiError' -import ExpiredTokenError from './ExpiredTokenError' -import { addBearer } from './util' +import Refresh from '../types/Refresh' +import apiCallFactory from './apiCallFactory' export interface AddStarDeps { accessToken: AccessToken + refresh: Refresh username: string slug: string } -const addStar = async ({ slug, accessToken, username }: AddStarDeps): Promise => { - const headers = new Headers() - addBearer(headers, accessToken) - const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, { - headers, - method: 'POST', - mode: 'cors' +const doAddStar = apiCallFactory('POST') + +const addStar = ({ accessToken, refresh, username, slug }: AddStarDeps) => + doAddStar({ + accessToken, + endpoint: `/recipes/${username}/${slug}/star`, + refresh }) - if (response.status === 401) { - throw new ExpiredTokenError() - } else if (!response.ok) { - throw new ApiError(response.status, response.statusText) - } -} export default addStar diff --git a/src/api/apiCallFactory.ts b/src/api/apiCallFactory.ts index 3c19489..9df38b9 100644 --- a/src/api/apiCallFactory.ts +++ b/src/api/apiCallFactory.ts @@ -2,38 +2,87 @@ import AccessToken from '../types/AccessToken' import Refresh from '../types/Refresh' import { ApiError } from './ApiError' -export interface ApiCallDeps { - accessToken: AccessToken | null - endpoint: string - query?: string - refresh: Refresh - signal: AbortSignal - body?: any -} - export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' -const getApiCallFactory = - (method: Method) => - (handleBody?: (raw: any) => T) => - async ({ accessToken, endpoint, refresh, signal, body }: ApiCallDeps): Promise => { +export type ApiCallFactoryDeps = WithHandleJson | WithHandleResponse + +export interface WithHandleJson { + handleJson: (raw: any) => T +} + +export interface WithHandleResponse { + handleResponse: (response: Response) => Promise | T +} + +export type ApiCallDeps = { + accessToken: AccessToken | null + refresh: Refresh + signal?: AbortSignal + body?: any +} & (WithEndpoint | WithUrl) + +export interface WithEndpoint { + endpoint: string + query?: string +} + +export interface WithUrl { + url: string +} + +export type ApiCall = (deps: ApiCallDeps) => Promise + +export interface ApiCallFactory { + (method: Method): ApiCall + (method: Method, handleBody: (raw: any) => T): ApiCall + (method: Method, deps: ApiCallFactoryDeps): ApiCall +} + +const handleResult = async ( + handleBodyOrDeps: ((raw: any) => T) | ApiCallFactoryDeps, + response: Response +): Promise => { + if (typeof handleBodyOrDeps === 'function') { + return handleBodyOrDeps(await response.json()) + } else { + const deps = handleBodyOrDeps + if ('handleResponse' in deps) { + return deps.handleResponse(response) + } else { + return deps.handleJson(await response.json()) + } + } +} + +const apiCallFactory = ( + method: Method, + handleBodyOrDeps?: ((raw: any) => T) | ApiCallFactoryDeps +): ApiCall => { + return (async (deps: ApiCallDeps): Promise => { + const { accessToken, refresh, signal } = deps const headers = new Headers() if (accessToken) { headers.set('Authorization', `Bearer ${accessToken.token}`) } - if (body) { + if (deps.body) { headers.set('Content-type', 'application/json') } - const url = import.meta.env.VITE_MME_API_URL + endpoint + const url = + 'url' in deps + ? deps.url + : deps.query + ? import.meta.env.VITE_MME_API_URL + deps.endpoint + '?' + deps.query + : import.meta.env.VITE_MME_API_URL + deps.endpoint + const body = deps.body ? JSON.stringify(deps.body) : undefined const response = await fetch(url, { - body: body ? JSON.stringify(body) : undefined, + body, signal, headers, mode: 'cors', method }) - if (response.ok && handleBody) { - return handleBody(await response.json()) + if (response.ok && handleBodyOrDeps) { + return handleResult(handleBodyOrDeps, response) } else if (response.status === 401) { const newToken = await refresh() if (newToken === null) { @@ -41,22 +90,21 @@ const getApiCallFactory = } headers.set('Authorization', `Bearer ${newToken.token}`) const retry = await fetch(url, { + body, signal, headers, mode: 'cors', method }) - if (retry.ok && handleBody) { - return handleBody(await retry.json()) - } else { + if (retry.ok && handleBodyOrDeps) { + return handleResult(handleBodyOrDeps, retry) + } else if (!retry.ok) { throw new ApiError(retry.status, retry.statusText) } - } else { + } else if (!response.ok) { throw new ApiError(response.status, response.statusText) } - } + }) as ApiCall +} -export const getCallFactory = getApiCallFactory('GET') -export const postCallFactory = getApiCallFactory('POST') -export const putCallFactory = getApiCallFactory('PUT') -export const deleteCallFactory = getApiCallFactory('DELETE') +export default apiCallFactory diff --git a/src/api/getImage.ts b/src/api/getImage.ts index 6c50337..f3accf2 100644 --- a/src/api/getImage.ts +++ b/src/api/getImage.ts @@ -1,31 +1,24 @@ import AccessToken from '../types/AccessToken' -import { ApiError } from './ApiError' -import ExpiredTokenError from './ExpiredTokenError' -import { addBearer } from './util' +import Refresh from '../types/Refresh' +import apiCallFactory from './apiCallFactory' export interface GetImageDeps { accessToken: AccessToken | null + refresh: Refresh signal: AbortSignal url: string } -const getImage = async ({ accessToken, signal, url }: GetImageDeps): Promise => { - const headers = new Headers() - if (accessToken !== null) { - addBearer(headers, accessToken) - } - const response = await fetch(url, { - headers, - mode: 'cors', - signal +const doGetImage = apiCallFactory('GET', { + handleResponse: async res => URL.createObjectURL(await res.blob()) +}) + +const getImage = async ({ accessToken, refresh, signal, url }: GetImageDeps) => + doGetImage({ + accessToken, + refresh, + signal, + url }) - if (response.ok) { - return URL.createObjectURL(await response.blob()) - } else if (response.status === 401) { - throw new ExpiredTokenError() - } else { - throw new ApiError(response.status, response.statusText) - } -} export default getImage diff --git a/src/api/getRecipe.ts b/src/api/getRecipe.ts index 5823b2a..aca2ffa 100644 --- a/src/api/getRecipe.ts +++ b/src/api/getRecipe.ts @@ -1,20 +1,18 @@ import AccessToken from '../types/AccessToken' -import { ApiError } from './ApiError' -import ExpiredTokenError from './ExpiredTokenError' +import Refresh from '../types/Refresh' +import apiCallFactory from './apiCallFactory' import GetRecipeView, { GetRecipeViewWithRawText, - RawGetRecipeView, - RawGetRecipeViewWithRawText, toGetRecipeView, toGetRecipeViewWithRawText } from './types/GetRecipeView' -import { addBearer } from './util' export interface GetRecipeCommonDeps { accessToken: AccessToken | null - username: string + refresh: Refresh slug: string - abortSignal: AbortSignal + signal: AbortSignal + username: string } export interface GetRecipeDeps extends GetRecipeCommonDeps { @@ -30,33 +28,33 @@ export interface GetRecipe { (deps: GetRecipeDepsIncludeRawText): Promise } +const doGetRecipe = apiCallFactory('GET', toGetRecipeView) +const doGetRecipeIncludeRawText = apiCallFactory('GET', toGetRecipeViewWithRawText) + const getRecipe = (async ({ accessToken, - username, + includeRawText, + refresh, slug, - abortSignal, - includeRawText + signal, + username }: GetRecipeDeps | GetRecipeDepsIncludeRawText): Promise => { - const headers = new Headers() - if (accessToken !== null) { - addBearer(headers, accessToken) - } - const query = includeRawText ? '?includeRawText=true' : '' - const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}${query}`, { - signal: abortSignal, - headers, - mode: 'cors' - }) - if (response.ok) { - if (includeRawText) { - return toGetRecipeViewWithRawText((await response.json()) as RawGetRecipeViewWithRawText) - } else { - return toGetRecipeView((await response.json()) as RawGetRecipeView) - } - } else if (response.status === 401) { - throw new ExpiredTokenError() + const endpoint = `/recipes/${username}/${slug}` + if (includeRawText) { + return doGetRecipeIncludeRawText({ + accessToken, + endpoint, + query: 'includeRawText=true', + refresh, + signal + }) } else { - throw new ApiError(response.status, response.statusText) + return doGetRecipe({ + accessToken, + endpoint, + refresh, + signal + }) } }) as GetRecipe diff --git a/src/api/getRecipeInfos.ts b/src/api/getRecipeInfos.ts index 67d4c99..d2224c9 100644 --- a/src/api/getRecipeInfos.ts +++ b/src/api/getRecipeInfos.ts @@ -1,6 +1,6 @@ import AccessToken from '../types/AccessToken' import Refresh from '../types/Refresh' -import { getCallFactory } from './apiCallFactory' +import apiCallFactory from './apiCallFactory' import { toRecipeInfosView } from './types/RecipeInfosView' export interface GetRecipeInfosDeps { @@ -11,7 +11,7 @@ export interface GetRecipeInfosDeps { signal: AbortSignal } -const doGetRecipeInfos = getCallFactory(toRecipeInfosView) +const doGetRecipeInfos = apiCallFactory('GET', toRecipeInfosView) const getRecipeInfos = ({ accessToken, pageNumber, pageSize, refresh, signal }: GetRecipeInfosDeps) => doGetRecipeInfos({ diff --git a/src/api/removeStar.ts b/src/api/removeStar.ts index 1725c57..d4ecd30 100644 --- a/src/api/removeStar.ts +++ b/src/api/removeStar.ts @@ -1,27 +1,21 @@ import AccessToken from '../types/AccessToken' -import { ApiError } from './ApiError' -import ExpiredTokenError from './ExpiredTokenError' -import { addBearer } from './util' +import Refresh from '../types/Refresh' +import apiCallFactory from './apiCallFactory' export interface RemoveStarDeps { accessToken: AccessToken + refresh: Refresh username: string slug: string } -const removeStar = async ({ accessToken, username, slug }: RemoveStarDeps) => { - const headers = new Headers() - addBearer(headers, accessToken) - const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, { - headers, - method: 'DELETE', - mode: 'cors' +const doRemoveStar = apiCallFactory('DELETE') + +const removeStar = ({ accessToken, refresh, username, slug }: RemoveStarDeps) => + doRemoveStar({ + accessToken, + endpoint: `/recipes/${username}/${slug}/star`, + refresh }) - if (response.status === 401) { - throw new ExpiredTokenError() - } else if (!response.ok) { - throw new ApiError(response.status, response.statusText) - } -} export default removeStar diff --git a/src/api/updateRecipe.ts b/src/api/updateRecipe.ts index de72130..c565c91 100644 --- a/src/api/updateRecipe.ts +++ b/src/api/updateRecipe.ts @@ -1,43 +1,31 @@ import AccessToken from '../types/AccessToken' -import { ApiError } from './ApiError' -import ExpiredTokenError from './ExpiredTokenError' -import { - GetRecipeViewWithRawText, - RawGetRecipeViewWithRawText, - toGetRecipeViewWithRawText -} from './types/GetRecipeView' +import Refresh from '../types/Refresh' +import apiCallFactory from './apiCallFactory' +import { GetRecipeViewWithRawText, toGetRecipeViewWithRawText } from './types/GetRecipeView' import UpdateRecipeSpec from './types/UpdateRecipeSpec' -import { addBearer } from './util' export interface UpdateRecipeDeps { spec: UpdateRecipeSpec accessToken: AccessToken + refresh: Refresh username: string slug: string } -const updateRecipe = async ({ +const doUpdateRecipe = apiCallFactory('POST', toGetRecipeViewWithRawText) + +const updateRecipe = ({ spec, accessToken, + refresh, username, slug -}: UpdateRecipeDeps): Promise => { - const headers = new Headers() - addBearer(headers, accessToken) - headers.set('Content-type', 'application/json') - const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}`, { - headers, - method: 'POST', - mode: 'cors', - body: JSON.stringify(spec) +}: UpdateRecipeDeps): Promise => + doUpdateRecipe({ + accessToken, + body: spec, + endpoint: `/recipes/${username}/${slug}`, + refresh }) - if (response.ok) { - return toGetRecipeViewWithRawText((await response.json()) as RawGetRecipeViewWithRawText) - } else if (response.status === 401) { - throw new ExpiredTokenError() - } else { - throw new ApiError(response.status, response.statusText) - } -} export default updateRecipe diff --git a/src/pages/edit-recipe/EditRecipe.tsx b/src/pages/edit-recipe/EditRecipe.tsx index 1f3b757..07336f5 100644 --- a/src/pages/edit-recipe/EditRecipe.tsx +++ b/src/pages/edit-recipe/EditRecipe.tsx @@ -7,6 +7,7 @@ import UpdateRecipeSpec, { fromFullRecipeView } from '../../api/types/UpdateReci import updateRecipe from '../../api/updateRecipe' import { useAuth } from '../../AuthProvider' import classes from './edit-recipe.module.css' +import { useRefresh } from '../../RefreshProvider' interface ControlProps { id: string @@ -88,6 +89,7 @@ export interface EditRecipeProps { const EditRecipe = ({ username, slug }: EditRecipeProps) => { const { accessToken } = useAuth() const navigate = useNavigate() + const refresh = useRefresh() // useEffect(() => { // if (auth.token === null) { @@ -106,10 +108,11 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => { queryFn: ({ signal }) => getRecipe({ accessToken, - username, + includeRawText: true, + refresh, slug, - abortSignal: signal, - includeRawText: true + signal, + username }) }, queryClient @@ -144,6 +147,7 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => { return updateRecipe({ spec, accessToken, + refresh, username, slug }) diff --git a/src/pages/recipe/Recipe.tsx b/src/pages/recipe/Recipe.tsx index 982fc6a..8a3c079 100644 --- a/src/pages/recipe/Recipe.tsx +++ b/src/pages/recipe/Recipe.tsx @@ -1,5 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { QueryObserverSuccessResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useNavigate } from '@tanstack/react-router' import addStar from '../../api/addStar' import { ApiError } from '../../api/ApiError' import getImage from '../../api/getImage' @@ -9,8 +10,8 @@ import GetRecipeView from '../../api/types/GetRecipeView' import { useAuth } from '../../AuthProvider' import RecipeVisibilityIcon from '../../components/recipe-visibility-icon/RecipeVisibilityIcon' import UserIconAndName from '../../components/user-icon-and-name/UserIconAndName' +import { useRefresh } from '../../RefreshProvider' import classes from './recipe.module.css' -import { useNavigate } from '@tanstack/react-router' interface EditButtonProps { username: string @@ -53,14 +54,16 @@ interface RecipeStarButtonProps { const RecipeStarButton = ({ username, slug, isStarred, starCount }: RecipeStarButtonProps) => { const { accessToken } = useAuth() const queryClient = useQueryClient() + const refresh = useRefresh() const addStarMutation = useMutation({ mutationFn: () => { if (accessToken !== null) { return addStar({ accessToken, - slug, - username + refresh, + username, + slug }) } else { return Promise.resolve() @@ -76,6 +79,7 @@ const RecipeStarButton = ({ username, slug, isStarred, starCount }: RecipeStarBu if (accessToken !== null) { return removeStar({ accessToken, + refresh, slug, username }) @@ -113,16 +117,18 @@ export interface RecipeProps { const Recipe = ({ username, slug }: RecipeProps) => { const { accessToken } = useAuth() const queryClient = useQueryClient() + const refresh = useRefresh() const recipeQuery = useQuery( { queryKey: ['recipes', username, slug], - queryFn: ({ signal: abortSignal }) => + queryFn: ({ signal }) => getRecipe({ - abortSignal, accessToken, - username, - slug + refresh, + signal, + slug, + username }) }, queryClient @@ -140,6 +146,7 @@ const Recipe = ({ username, slug }: RecipeProps) => { getImage({ accessToken, signal, + refresh, url: recipeQuery.data!.recipe.mainImage!.url }) }, diff --git a/src/pages/recipes/Recipes.tsx b/src/pages/recipes/Recipes.tsx index 0687567..eefb816 100644 --- a/src/pages/recipes/Recipes.tsx +++ b/src/pages/recipes/Recipes.tsx @@ -46,6 +46,7 @@ const Recipes = () => { // any needed in the params const imgUrl = await getImage({ accessToken, + refresh, signal, url: recipeInfoView.mainImage!.url })