This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ComponentRendering, LayoutServicePageState } from '@sitecore-jss/sitecore-jss-nextjs'; | |
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; | |
import { ParsedUrlQuery } from 'querystring'; | |
import { User } from '@/interfaces/user'; | |
import { mergeAxiosResponseArrays } from '@/lib/helpers'; | |
import RedisClient from '@/lib/redis-client'; | |
import { StyleguideSitecoreContextValue } from './component-props'; | |
interface Service { | |
[key: string]: { | |
get: (params: Record<string, unknown>, ...args: unknown[]) => string; | |
getCallback?: (params: Record<string, unknown>, response: AxiosResponse, ...args: unknown[]) => string; | |
getRedis?: (params: Record<string, unknown>, ...args: unknown[]) => Promise<AxiosResponse<unknown>>; | |
pushRedis?: (params: Record<string, unknown>, ...args: unknown[]) => Promise<void>; | |
}; | |
} | |
interface LayoutWithService { | |
[key: string]: { name: string; queryParams: string[]; pathMapping: string[]; contextName?: string; }[]; | |
} | |
enum APISources { AIS_Products, AIS_Generic, Sitecore, Next } | |
/** | |
* Gets the required API host based on sitecore page state and API source | |
*/ | |
const getAPIHost = (pageState: string, sourceName: APISources) => { | |
const sources: { name: APISources; host?: string; mockHost?: string }[] = [ | |
{ | |
name: APISources.AIS_Products, | |
host: process.env.AIS_HOST, | |
mockHost: process.env.AIS_MOCK_HOST, | |
}, | |
{ | |
name: APISources.AIS_Generic, | |
host: process.env.AIS_BASE_URL, | |
mockHost: process.env.AIS_MOCK_HOST, | |
}, | |
{ | |
name: APISources.Sitecore, | |
host: process.env.SITECORE_CUSTOMAPI_HOST, | |
mockHost: process.env.SITECORE_USTOMAPI_MOCK_HOST, | |
}, | |
{ | |
name: APISources.Next, | |
host: `${process.env.PUBLIC_NEXT_API_URL}/api`, | |
mockHost: process.env.AIS_MOCK_HOST, | |
}, | |
]; | |
const selectedSource = sources.find((source) => source.name === sourceName); | |
return pageState === LayoutServicePageState.Normal | |
? selectedSource?.host | |
: selectedSource?.mockHost; | |
}; | |
/** | |
* Gets response from Redis | |
*/ | |
const getRedis = async (key: string, pageState: string, contextName?: string) => { | |
if (pageState !== LayoutServicePageState.Normal) { | |
return null; | |
} | |
const client = RedisClient.getRedisClient(); | |
const value = await client.get(key); | |
const parsedValue = JSON.parse(value ?? 'null'); | |
return contextName ? { [contextName]: parsedValue } : parsedValue; | |
}; | |
/** | |
* Pushes value to Redis | |
*/ | |
export const pushRedis = async ( | |
key: string, | |
value: string, | |
pageState: string, | |
useLongExpiration = false, | |
) => { | |
const expirationTime = process.env[useLongExpiration ? 'REDIS_EXPIRE_LONG' : 'REDIS_EXPIRE'] ?? '60'; | |
if (pageState !== LayoutServicePageState.Normal) { | |
return; | |
} | |
const client = RedisClient.getRedisClient(); | |
await client.set(key, value, { EX: Number.parseInt(expirationTime), }); | |
}; | |
// List of each service and its API endpoint | |
export const services: Service = { | |
products: { | |
get: (params: { userId: string; pageState: string }) => | |
`${getAPIHost(params.pageState, APISources.AIS_Products)}/user/${ | |
params.userId | |
}/products`, | |
getRedis: ({ userId, pageState }: { userId: string; pageState: string }) => | |
getRedis(`${userId}.products`, pageState), | |
pushRedis: ({ userId, value, pageState, | |
}: { | |
userId: string; | |
value: string; | |
pageState: string; | |
}) => pushRedis(`${userId}.products`, value, pageState), | |
}, | |
reservations: { | |
get: (params: { userId: string; pageState: string }) => | |
`${getAPIHost(params.pageState, APISources.Next)}/user/${params.userId}/reservations`, | |
getRedis: ({ userId, pageState }: { userId: string; pageState: string }) => | |
getRedis(`${userId}.reservations`, pageState), | |
pushRedis: ({ userId, value, pageState }: { | |
userId: string; | |
value: string; | |
pageState: string; | |
}) => pushRedis(`${userId}.reservations`, value, pageState), | |
}, | |
countries: { | |
get: (params: { pageState: string }) => | |
`${getAPIHost(params.pageState, APISources.AIS_Generic)}/location/v1/countries`, | |
getRedis: ({ pageState, contextName }: { pageState: string; contextName: string }) => | |
getRedis('countries', pageState, contextName), | |
pushRedis: ({ value, pageState }: { value: string; pageState: string }) => | |
pushRedis('countries', value, pageState, true), | |
}, | |
}; | |
// List of possible layouts with their associated services | |
export const layoutServices: LayoutWithService = { | |
Products: [{ name: 'products', queryParams: [], pathMapping: [] }], | |
ProductDetails: [{ name: 'products', queryParams: [], pathMapping: [] }], | |
ProductFinder: [ | |
{ name: 'reservations', queryParams: [], pathMapping: [] }, | |
{ name: 'products', queryParams: [], pathMapping: [] }, | |
], | |
}; | |
/** | |
* Maps the Sitecore components (or layouts) to a list | |
*/ | |
export const getLayoutsFromSitecore = ( | |
sitecoreContext: StyleguideSitecoreContextValue | null, | |
): string[] => { | |
if (!sitecoreContext) { | |
return []; | |
} | |
const jssMain = sitecoreContext.route.placeholders['jss-main'].filter((rendering) => (rendering as ComponentRendering)?.componentName)[0] as ComponentRendering; | |
const jssContent = jssMain?.placeholders?.['jss-content'] as ComponentRendering[]; | |
return jssContent?.map((content) => content.componentName); | |
}; | |
/** | |
* Maps the services needs to be called to a list | |
*/ | |
export const getServicesByLayouts = (sitecoreContext: StyleguideSitecoreContextValue | null) => { | |
const layouts = getLayoutsFromSitecore(sitecoreContext); | |
return layouts?.map((layout) => layoutServices[layout]).flat().filter(Boolean); | |
}; | |
/** | |
* Maps browser path by given list of path mapping to object | |
*/ | |
export const getPathMappingFromService = (path: string[], pathMapping: string[]) => { | |
return ( | |
pathMapping?.reduce((prev, currentPath, i) => { | |
prev[currentPath] = path[i]; | |
return prev; | |
}, {} as Record<string, string>) || {} | |
); | |
}; | |
/** | |
* | |
* @param sitecoreContext The actual rendering context of Sitecore | |
*/ | |
export const mapServicesToRequests = ( | |
sitecoreContext: StyleguideSitecoreContextValue | null, | |
queryParams: ParsedUrlQuery, | |
user?: User, | |
): Promise<AxiosResponse<unknown>>[] => { | |
const servicesToCall = getServicesByLayouts(sitecoreContext) || []; | |
const { path, ...getParams } = queryParams; | |
const params = getParams as Record<string, string>; | |
params.pageState = sitecoreContext?.pageState?.toString() ?? ''; | |
return servicesToCall.map(async (service) => { | |
const mappedPath = getPathMappingFromService(path as string[], service.pathMapping); | |
const foundService = services[service.name]; | |
if (foundService?.getRedis) { | |
// 1. try to load the response from Redis | |
let response: AxiosResponse<unknown> | null = null; | |
const redisCall = foundService | |
?.getRedis({ ...params, ...user, ...mappedPath, contextName: service?.contextName }) | |
.then((redisResponse) => { | |
response = redisResponse; | |
return new Promise<AxiosResponse<unknown>>((resolve) => | |
resolve({ data: redisResponse } as unknown as AxiosResponse<unknown>), | |
); | |
}); | |
await redisCall; | |
if (response) { | |
// return the response from Redis if it has a response | |
return new Promise<AxiosResponse<unknown>>((resolve) => | |
resolve({ data: response } as unknown as AxiosResponse<unknown>), | |
); | |
} | |
} | |
return axios | |
.get( | |
// 2. load the response from AIS | |
foundService.get({ ...params, ...user, ...mappedPath }), | |
{ headers: { contextName: service?.contextName || null } }, | |
) | |
.then((axiosResponse) => { | |
// 3. load and merge response from callback, if any | |
if (foundService.getCallback && axiosResponse.status === 200 && axiosResponse.data) { | |
return axios | |
.get( | |
foundService.getCallback( | |
{ | |
...params, | |
...user, | |
...mappedPath, | |
}, | |
axiosResponse, | |
), | |
{ headers: { contextName: service?.contextName || null } }, | |
) | |
.then((callbackResponse) => { | |
if (callbackResponse.status === 200 && callbackResponse.data) { | |
axiosResponse.data = { | |
[service.name]: mergeAxiosResponseArrays(axiosResponse, callbackResponse), | |
}; | |
} | |
return axiosResponse; | |
}); | |
} | |
return axiosResponse; | |
}) | |
.then((axiosResponse) => { | |
// 4. push the response from AIS to Redis | |
if (axiosResponse.status === 200 && axiosResponse.data) { | |
params.value = JSON.stringify(axiosResponse.data); | |
foundService?.pushRedis?.({ ...params, ...user, ...mappedPath }); | |
} | |
return axiosResponse; | |
}); | |
}); | |
}; | |
/** | |
* Handles errors came from API calls | |
*/ | |
export const handleGetServerSidePropsDataFetchError = (error: Error | AxiosError) => { | |
if (axios.isAxiosError(error)) { | |
console.error(error); | |
return { | |
redirect: { | |
permanent: false, | |
destination: `/${error?.response?.status || 500}`, | |
}, | |
props: {}, | |
}; | |
} else { | |
return { | |
redirect: { | |
permanent: false, | |
destination: '/500', | |
}, | |
}; | |
} | |
}; | |
/** | |
* Maps a list of API response data to an object | |
*/ | |
export const reduceAxiosResponses = <T>(responses: AxiosResponse<T>[]) => | |
responses | |
.map(({ data, config }) => [data, config]) | |
.reduce((allData, [data, config]) => { | |
const contextName = (config as AxiosRequestConfig)?.headers?.contextName; | |
// Wrap data in a field for easier access in state manager if contextName is provided | |
const transformedData = contextName ? { [contextName]: data } : data; | |
return { ...allData, ...transformedData }; | |
}, {}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment