Updated all api calls to use new apiCallFactory.
This commit is contained in:
parent
c54d3832a3
commit
a3376a8cc1
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
const endpoint = `/recipes/${username}/${slug}`
|
||||
if (includeRawText) {
|
||||
return toGetRecipeViewWithRawText((await response.json()) as RawGetRecipeViewWithRawText)
|
||||
return doGetRecipeIncludeRawText({
|
||||
accessToken,
|
||||
endpoint,
|
||||
query: 'includeRawText=true',
|
||||
refresh,
|
||||
signal
|
||||
})
|
||||
} else {
|
||||
return toGetRecipeView((await response.json()) as RawGetRecipeView)
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
throw new ExpiredTokenError()
|
||||
} else {
|
||||
throw new ApiError(response.status, response.statusText)
|
||||
return doGetRecipe({
|
||||
accessToken,
|
||||
endpoint,
|
||||
refresh,
|
||||
signal
|
||||
})
|
||||
}
|
||||
}) as GetRecipe
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
})
|
||||
},
|
||||
|
@ -46,6 +46,7 @@ const Recipes = () => {
|
||||
// any needed in the params
|
||||
const imgUrl = await getImage({
|
||||
accessToken,
|
||||
refresh,
|
||||
signal,
|
||||
url: recipeInfoView.mainImage!.url
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user