diff --git a/framework/ordercloud/api/utils/fetch.ts b/framework/ordercloud/api/utils/fetch.ts index 9d9fff3ed..e6abf15a3 100644 --- a/framework/ordercloud/api/utils/fetch.ts +++ b/framework/ordercloud/api/utils/fetch.ts @@ -1,3 +1,146 @@ -import zeitFetch from '@vercel/fetch' +import vercelFetch from '@vercel/fetch' +import { FetcherError } from '@commerce/utils/errors' +import jwt from 'jsonwebtoken' -export default zeitFetch() +import { OrdercloudConfig } from '../index' + +// Get an instance to vercel fetch +const fetch = vercelFetch() + +// Get token util +async function getToken(baseUrl: string) { + // If not, get a new one and store it + const authResponse = await fetch(`${baseUrl}/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: `client_id=${process.env.NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID}&grant_type=client_credentials`, + }) + + // If something failed getting the auth response + if (!authResponse.ok) { + // Get the body of it + const error = await authResponse.json() + + // And return an error + throw new FetcherError({ + errors: [{ message: error.error_description.Code }], + status: error.error_description.HttpStatus, + }) + } + + // Return the token + return authResponse.json().then((response) => response.access_token) +} + +export async function fetchData( + opts: { + path: string + method: string + baseUrl: string + fetchOptions?: Record + body?: Record + }, + retries = 0 +): Promise { + // Destructure opts + const { path, body, fetchOptions, baseUrl, method = 'GET' } = opts + + // Decode token + const decoded = jwt.decode(global.token as string) as jwt.JwtPayload | null + + // If token is not present or its expired, get a new one and store it + if ( + !global.token || + (typeof decoded?.exp === 'number' && decoded?.exp * 1000 < +new Date()) + ) { + // Get a new one + const token = await getToken(baseUrl) + + // Store it + global.token = token + } + + // Do the request with the correct headers + const dataResponse = await fetch(`${baseUrl}/v1${path}`, { + ...fetchOptions, + method, + headers: { + ...fetchOptions?.headers, + 'Content-Type': 'application/json', + accept: 'application/json, text/plain, */*', + authorization: `Bearer ${global.token}`, + }, + body: body ? JSON.stringify(body) : undefined, + }) + + // If something failed getting the data response + if (!dataResponse.ok) { + // Log error + console.log(await dataResponse.textConverted()) + + // If token is expired + if (dataResponse.status === 401) { + // Get a new one + const token = await getToken(baseUrl) + + // Store it + global.token = token + } + + // And if retries left + if (retries < 2) { + // Refetch + return fetchData(opts, retries + 1) + } + + // Get the body of it + const error = await dataResponse.json() + + // And return an error + throw new FetcherError({ + errors: [{ message: error.error_description.Code }], + status: error.error_description.HttpStatus, + }) + } + + try { + // Return data response as json + return (await dataResponse.json()) as Promise + } catch (error) { + // If response is empty return it as text + return null as unknown as Promise + } +} + +const serverFetcher: ( + getConfig: () => OrdercloudConfig +) => ( + method: string, + path: string, + body?: Record, + fetchOptions?: Record +) => Promise = + (getConfig) => + async ( + method: string, + path: string, + body?: Record, + fetchOptions?: Record + ) => { + // Get provider config + const { commerceUrl } = getConfig() + + // Return the data and specify the expected type + return fetchData({ + fetchOptions, + method, + baseUrl: commerceUrl, + path, + body, + }) + } + +export default serverFetcher diff --git a/framework/ordercloud/fetcher.ts b/framework/ordercloud/fetcher.ts index ba6fd5bf7..6f314a71e 100644 --- a/framework/ordercloud/fetcher.ts +++ b/framework/ordercloud/fetcher.ts @@ -1,7 +1,17 @@ import { Fetcher } from '@commerce/utils/types' -export const fetcher: Fetcher = async () => { - throw new Error( - 'Client side fetching has not been implemented yet, try to fetch from server side.' - ) +const clientFetcher: Fetcher = async ({ method, url, body }) => { + const response = await fetch(url!, { + method, + body: body ? JSON.stringify(body) : undefined, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => response.json()) + .then((response) => response.data) + + return response } + +export default clientFetcher