Updated all api calls to use new apiCallFactory.

This commit is contained in:
Jesse Brault 2024-08-22 08:00:10 -05:00
parent c54d3832a3
commit a3376a8cc1
10 changed files with 173 additions and 146 deletions

View File

@ -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<void> => {
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<void>('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

View File

@ -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) =>
<T>(handleBody?: (raw: any) => T) =>
async ({ accessToken, endpoint, refresh, signal, body }: ApiCallDeps): Promise<T> => {
export type ApiCallFactoryDeps<T> = WithHandleJson<T> | WithHandleResponse<T>
export interface WithHandleJson<T> {
handleJson: (raw: any) => T
}
export interface WithHandleResponse<T> {
handleResponse: (response: Response) => Promise<T> | 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<T> = (deps: ApiCallDeps) => Promise<T>
export interface ApiCallFactory<T> {
(method: Method): ApiCall<void>
(method: Method, handleBody: (raw: any) => T): ApiCall<T>
(method: Method, deps: ApiCallFactoryDeps<T>): ApiCall<T>
}
const handleResult = async <T>(
handleBodyOrDeps: ((raw: any) => T) | ApiCallFactoryDeps<T>,
response: Response
): Promise<T> => {
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 = <T>(
method: Method,
handleBodyOrDeps?: ((raw: any) => T) | ApiCallFactoryDeps<T>
): ApiCall<typeof handleBodyOrDeps extends undefined ? void : T> => {
return (async (deps: ApiCallDeps): Promise<void | T> => {
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<T>
}
export const getCallFactory = getApiCallFactory('GET')
export const postCallFactory = getApiCallFactory('POST')
export const putCallFactory = getApiCallFactory('PUT')
export const deleteCallFactory = getApiCallFactory('DELETE')
export default apiCallFactory

View File

@ -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<string> => {
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

View File

@ -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<GetRecipeViewWithRawText>
}
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<GetRecipeView | GetRecipeViewWithRawText> => {
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

View File

@ -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({

View File

@ -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<void>('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

View File

@ -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<GetRecipeViewWithRawText> => {
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<GetRecipeViewWithRawText> =>
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

View File

@ -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
})

View File

@ -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
})
},

View File

@ -46,6 +46,7 @@ const Recipes = () => {
// any needed in the params
const imgUrl = await getImage({
accessToken,
refresh,
signal,
url: recipeInfoView.mainImage!.url
})