Skip to content

Instantly share code, notes, and snippets.

@trnktms
Last active Jun 21, 2022
Embed
What would you like to do?
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