Created
July 8, 2022 07:50
-
-
Save pzi/2051a5eff32bcc458c67a92a86d7b030 to your computer and use it in GitHub Desktop.
Refactored Layout Service Factory to handle query params
This file contains hidden or 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 {placeholderHasQueryParam, processServerUrl, processPlaceholderUrl} from './layout-service-factory'; | |
/** | |
* @jest-environment node | |
*/ | |
jest.mock('@sitecore-jss/sitecore-jss-nextjs', () => ({ | |
getPublicUrl: jest.fn().mockReturnValue('http://dev.url'), | |
})); | |
test('placeholderHasQueryParam', () => { | |
expect(placeholderHasQueryParam('http://localhost:3000')).toBe(false); | |
expect(placeholderHasQueryParam('http://localhost:3000?foo=bar')).toBe(false); | |
expect(placeholderHasQueryParam('http://dev.url?item=/why-we-are-fast?utm_medium=social&foo=bar')).toBe(true); | |
expect(placeholderHasQueryParam('http://dev.url?item=%2Fwhy-we-are-fast%3Futm_medium%3Dsocial')).toBe(true); | |
expect( | |
placeholderHasQueryParam( | |
'http://dev.url/sitecore/api/layout/placeholder/jss?placeholderName=hpa-ph-header&item=%2Fdevcontent%3Fbeep=boop&sc_apikey=%herpderp%7D&sc_site=some-app&sc_lang=&tracking=false', | |
), | |
).toBe(true); | |
expect( | |
placeholderHasQueryParam( | |
'https://dev.url/sitecore/api/layout/placeholder/jss?placeholderName=hpa-ph-config&item=%2Fconfig%3Fbeep=bopp&sc_apikey=%herpderp%7D&sc_site=some-app&sc_lang=en&tracking=false', | |
), | |
).toBe(true); | |
}); | |
test('processPlaceholderUrl', () => { | |
expect(processPlaceholderUrl('http://dev.url?item=/why-we-are-fast?utm_medium=social&foo=bar')).toBe( | |
'http://dev.url/?item=%2Fwhy-we-are-fast&foo=bar&utm_medium=social', | |
); | |
expect( | |
processPlaceholderUrl( | |
'/sitecore/api/layout/placeholder/jss?placeholderName=hpa-ph-config&item=%2Fconfig%3Fbeep=bopp&sc_apikey=%herpderp%7D&sc_site=some-app&sc_lang=en&tracking=false', | |
), | |
).toBe( | |
'http://dev.url/sitecore/api/layout/placeholder/jss?placeholderName=hpa-ph-config&item=%2Fconfig&sc_apikey=%25herpderp%7D&sc_site=some-app&sc_lang=en&tracking=false&beep=bopp', | |
); | |
expect( | |
processPlaceholderUrl( | |
'http://dev.url/sitecore/api/layout/placeholder/jss?placeholderName=hpa-ph-header&item=%2Fdevcontent%3Fbeep=boop&sc_apikey=%herpderp%7D&sc_site=some-app&sc_lang=&tracking=false', | |
), | |
).toBe( | |
'http://dev.url/sitecore/api/layout/placeholder/jss?placeholderName=hpa-ph-header&item=%2Fdevcontent&sc_apikey=%25herpderp%7D&sc_site=some-app&sc_lang=&tracking=false&beep=boop', | |
); | |
expect( | |
processPlaceholderUrl('http://dev.url?item=%2Fconfig%3Fsc_apikey=attempted_override&sc_apikey=%herpderp%7D'), | |
).toBe('http://dev.url/?item=%2Fconfig&sc_apikey=%25herpderp%7D'); | |
}); | |
test('processServerUrl', () => { | |
expect(processServerUrl('/some-path', 'http://dev.url/some-path')).toBe('http://dev.url/some-path'); | |
expect(processServerUrl('/some-path?foo=new', 'http://dev.url/some-path?foo=original')).toBe( | |
'http://dev.url/some-path?foo=original', | |
); | |
expect(processServerUrl('/some-path?foo=bar', 'http://dev.url/some-path')).toBe('http://dev.url/some-path?foo=bar'); | |
expect(processServerUrl('/some-path?foo=bar', 'http://dev.url/some-path?baz=qux')).toBe( | |
'http://dev.url/some-path?baz=qux&foo=bar', | |
); | |
}); |
This file contains hidden or 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 type {IncomingMessage, ServerResponse} from 'http'; | |
import {URL as NodeURL} from 'url'; | |
import {debug} from '@sitecore-jss/sitecore-jss'; | |
import type {AxiosDataFetcherConfig, RestLayoutServiceConfig} from '@sitecore-jss/sitecore-jss-nextjs'; | |
import {AxiosDataFetcher, getPublicUrl, RestLayoutService} from '@sitecore-jss/sitecore-jss-nextjs'; | |
import type {DataFetcherResolver} from '@sitecore-jss/sitecore-jss/layout'; | |
import type {AxiosRequestConfig, AxiosResponse, HeadersDefaults} from 'axios'; | |
import sitecoreConfig from 'temp/config'; | |
import {isServer} from 'utils/environment'; | |
const publicUrl = getPublicUrl(); | |
// Either use Node URL or Browser URL as the processing might happen in the browser | |
const SafeURL = isServer() ? NodeURL : URL; | |
interface TypedAxiosRequestConfig extends Omit<AxiosRequestConfig, 'headers'> { | |
headers: HeadersDefaults; | |
} | |
// We are duplicating a bunch of code from the jss repo so we can override the fetcher method. | |
// All is a copy-paste, except for the returned fetcher method called out below | |
// @see: https://github.com/Sitecore/jss/blob/4e85a70c521f76535b3b36f73bcc9f69874d2408/packages/sitecore-jss/src/layout/rest-layout-service.ts | |
const setupReqHeaders = (req: IncomingMessage) => { | |
return (reqConfig: TypedAxiosRequestConfig) => { | |
debug.layout('performing request header passing'); | |
if (reqConfig.headers) { | |
reqConfig.headers.common = { | |
...reqConfig.headers.common, | |
...(req.headers.cookie && {cookie: req.headers.cookie}), | |
...(req.headers.referer && {referer: req.headers.referer}), | |
...(req.headers['user-agent'] && {'user-agent': req.headers['user-agent']}), | |
...(req.socket.remoteAddress && {'X-Forwarded-For': req.socket.remoteAddress}), | |
}; | |
} | |
return reqConfig; | |
}; | |
}; | |
const setupResHeaders = (res: ServerResponse) => { | |
return (serverRes: AxiosResponse) => { | |
debug.layout('performing response header passing'); | |
serverRes.headers['set-cookie'] && res.setHeader('set-cookie', serverRes.headers['set-cookie']); | |
return serverRes; | |
}; | |
}; | |
export const dataFetcherResolver: DataFetcherResolver = <T>(req?: IncomingMessage, res?: ServerResponse) => { | |
const config = { | |
debugger: debug.layout, | |
} as AxiosDataFetcherConfig; | |
if (req && res) { | |
config.onReq = setupReqHeaders(req) as AxiosDataFetcherConfig['onReq']; | |
config.onRes = setupResHeaders(res) as AxiosDataFetcherConfig['onRes']; | |
} | |
const axiosFetcher = new AxiosDataFetcher(config); | |
// Here is our custom implementation of the fetcher function | |
return (fetchUrl: string, data?: unknown) => { | |
fetchUrl = processURLQueryParams(fetchUrl, req?.url); | |
return axiosFetcher.fetch<T>(fetchUrl, data); | |
}; | |
}; | |
export class LayoutServiceFactory { | |
create(additionalConfig?: Partial<RestLayoutServiceConfig>) { | |
return new RestLayoutService({ | |
apiHost: sitecoreConfig.sitecoreApiHost, | |
apiKey: sitecoreConfig.nextPublicSitecoreApiKey, | |
siteName: sitecoreConfig.jssAppName, | |
configurationName: 'jss', | |
tracking: false, | |
dataFetcherResolver, | |
...additionalConfig, | |
}); | |
} | |
} | |
export const processURLQueryParams = (fetchUrl: string, requestUrl: string | undefined): string => { | |
// if we receive a requestUrl, then we obtained it from a Server context | |
if (requestUrl) { | |
return processServerUrl(requestUrl, fetchUrl); | |
} | |
if (placeholderHasQueryParam(fetchUrl)) { | |
return processPlaceholderUrl(fetchUrl); | |
} | |
return fetchUrl; | |
}; | |
/** | |
* Method that takes a Server request url e.g `/some-path?status=1` and adds the query params to the | |
* url that is used to fetch from the layout service if they don't exist yet. | |
*/ | |
export const processServerUrl = (requestUrl: string, fetchUrl: string): string => { | |
if (!requestUrl.includes('_next')) { | |
const requestURL = new SafeURL(requestUrl, publicUrl); | |
// if we have get params on our request url | |
// forward them onto the call to the layout service | |
if (requestURL.search) { | |
const fetchURL = new SafeURL(fetchUrl); | |
fetchUrl = getNewCompleteURL(requestURL.searchParams, fetchURL); | |
} | |
} | |
return fetchUrl; | |
}; | |
export const placeholderHasQueryParam = (url: string): boolean => getRequestURLs(url).itemURL.search !== ''; | |
/** | |
* Method that processes a placeholder item that contains a query string, | |
* extracts it and attaches it to the end of the URL. | |
* @example `dev.url?item=%path%3Fquery=string` -> `dev.url?item=%path&query=string` | |
* @example `dev.url?item=%path%3Fquery=string&foo=bar` -> `dev.url?item=%path&foo=bar&query=string` | |
*/ | |
export const processPlaceholderUrl = (url: string): string => { | |
const { | |
completeURL, | |
itemURL: {pathname: itemPath, searchParams: itemQueryParams}, | |
} = getRequestURLs(url); | |
// update the existing item with the path (without query params) and delete all other `item` params if any. | |
completeURL.searchParams.set('item', itemPath); | |
return getNewCompleteURL(itemQueryParams, completeURL); | |
}; | |
/** | |
* Helper to create URL objects of incoming request urls | |
*/ | |
const getRequestURLs = (url: string): {completeURL: URL; itemURL: URL} => { | |
// Create URL object to parse incoming string | |
const completeURL = new SafeURL(url, publicUrl); | |
const itemParam = completeURL.searchParams.get('item') ?? ''; | |
// Grab the item query string | |
return { | |
itemURL: new SafeURL(itemParam, publicUrl), | |
completeURL, | |
}; | |
}; | |
/** | |
* Util to safely add non-existent query params to a given URL | |
*/ | |
const getNewCompleteURL = (params: URLSearchParams, completeURL: URL): string => { | |
const newURL = new SafeURL(completeURL.href); | |
params.forEach((value, name) => { | |
if (!newURL.searchParams.has(name)) { | |
newURL.searchParams.append(name, value); | |
} | |
}); | |
return newURL.href; | |
}; | |
export const layoutServiceFactory = new LayoutServiceFactory(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment