Skip to content

Commit

Permalink
feat: prepareFetchHookData
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasio committed Mar 21, 2024
1 parent 654681d commit 8ffb944
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-kids-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@headstartwp/next": patch
---

Introducing `prepareFetchHookData` and making it public as it can be useful for projects customizing `fetchHookData` behavior.
2 changes: 1 addition & 1 deletion packages/next/src/data/convertToPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ export function convertToPath(args: string[] | undefined) {
return '/';
}

return `/${args.join('/')}`;
return `/${args.filter((a) => a.length > 0).join('/')}`;
}
97 changes: 96 additions & 1 deletion packages/next/src/data/server/__tests__/fetchHookData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useSearch } from '../../hooks/useSearch';
import { useTerms } from '../../hooks/useTerms';
import { usePost } from '../../hooks/usePost';
import { usePostOrPosts } from '../../hooks/usePostOrPosts';
import { fetchHookData } from '../fetchHookData';
import { fetchHookData, prepareFetchHookData } from '../fetchHookData';
import { addHookData } from '../addHookData';

test('fetchHookData types', async () => {
Expand All @@ -31,6 +31,101 @@ test('fetchHookData types', async () => {
).toMatchTypeOf<PostEntity>();
});

describe('prepareFetchHookData', () => {
it('builds path correctly', () => {
const { path } = prepareFetchHookData(usePosts.fetcher(), {
params: {
path: ['2024', '01', '02', 'page-name'],
},
});

expect(path).toBe('/2024/01/02/page-name');

const { path: path2 } = prepareFetchHookData(usePosts.fetcher(), {
params: {
path: ['2024', '01', '02', 'page-name', '', ''],
},
});

expect(path2).toBe('/2024/01/02/page-name');
});

it('returns url params properly', () => {
const { urlParams } = prepareFetchHookData(usePost.fetcher(), {
params: {
path: ['2024', '01', '02', 'page-name'],
},
});

expect(urlParams).toMatchObject({ slug: 'page-name' });

const { urlParams: urlParams2 } = prepareFetchHookData(usePosts.fetcher(), {
params: {
path: ['category', 'news'],
},
});

expect(urlParams2).toMatchObject({ category: 'news' });
});

it('returns params properly', () => {
setHeadstartWPConfig({
useWordPressPlugin: true,
integrations: {
polylang: {
enable: true,
},
},
});

const { params } = prepareFetchHookData(
usePost.fetcher(),
{
// params from next.js ctx
params: {
path: ['2024', '01', '02', 'page-name'],
},
locale: 'pt',
},
{
// user passed params
params: {
id: 1,
},
},
);

expect(params).toEqual({ slug: 'page-name', _embed: true, id: 1, lang: 'pt' });

setHeadstartWPConfig({
useWordPressPlugin: true,
});
});

it('returns cacheKey properly', () => {
const { cacheKey } = prepareFetchHookData(
usePost.fetcher(),
{
// params from next.js ctx
params: {
path: ['2024', '01', '02', 'page-name'],
},
},
{
// user passed params
params: {
id: 1,
},
},
);

expect(cacheKey).toMatchObject({
args: { _embed: true, id: 1, slug: 'page-name', sourceUrl: '' },
url: '/wp-json/wp/v2/posts',
});
});
});

describe('fetchHookData', () => {
it('handles additionalCacheObjects', async () => {
setHeadstartWPConfig({
Expand Down
106 changes: 65 additions & 41 deletions packages/next/src/data/server/fetchHookData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,49 @@ function isPreviewRequest<P>(params: P, urlParams: P): params is P & PostParams
return isPreviewRequest;
}

/**
* Prepares all the things for fetchHookData
*
* @param fetchStrategy The fetch strategy to use. Typically this is exposed by the hook e.g: `usePosts.fetcher()`
* @param ctx The Next.js context, either the one from `getServerSideProps` or `getStaticProps`
* @param options See {@link FetchHookDataOptions}
*
* @returns The various things fetchHookData needs
*/
export function prepareFetchHookData<T = unknown, P extends EndpointParams = EndpointParams, R = T>(
fetchStrategy: AbstractFetchStrategy<T, P, R>,
ctx: GetServerSidePropsContext<any, PreviewData> | GetStaticPropsContext<any, PreviewData>,
options: FetchHookDataOptions<P, T> = {},
) {
const { sourceUrl, integrations } = getSiteFromContext(ctx);
const params: Partial<P> = options?.params || {};

fetchStrategy.setBaseURL(sourceUrl);

let path: string[] = [];

if (ctx.params) {
path = Array.isArray(ctx.params.path) ? ctx.params.path : [ctx.params.path || ''];
}

if (integrations?.polylang?.enable && (ctx.locale || ctx.defaultLocale)) {
params.lang = ctx.locale ?? ctx.defaultLocale;
}

const stringPath = convertToPath(path);
const defaultParams = fetchStrategy.getDefaultParams();
const urlParams = fetchStrategy.getParamsFromURL(stringPath, params);

const finalParams = deepmerge.all([defaultParams, urlParams, params]) as Partial<P>;

return {
cacheKey: fetchStrategy.getCacheKey(finalParams),
params: finalParams,
urlParams,
path: stringPath,
};
}

/**
* A function that implements data fetching on the server. This should be used in `getServerSideProps`
* or `getStaticProps`.
Expand All @@ -67,7 +110,7 @@ function isPreviewRequest<P>(params: P, urlParams: P): params is P & PostParams
* } catch (e) {
* return handleError(e, context);
* }
* }
* }´
* ```
*
* @param fetchStrategy The fetch strategy to use. Typically this is exposed by the hook e.g: `usePosts.fetcher()`
Expand All @@ -83,43 +126,28 @@ export async function fetchHookData<T = unknown, P extends EndpointParams = Endp
ctx: GetServerSidePropsContext<any, PreviewData> | GetStaticPropsContext<any, PreviewData>,
options: FetchHookDataOptions<P, T> = {},
) {
const { sourceUrl, integrations, debug, preview } = getSiteFromContext(ctx);
const params: Partial<P> = options?.params || {};

fetchStrategy.setBaseURL(sourceUrl);
const {
cacheKey: key,
params,
urlParams,
path,
} = prepareFetchHookData(fetchStrategy, ctx, options);

let path: string[] = [];

if (ctx.params) {
path = Array.isArray(ctx.params.path) ? ctx.params.path : [ctx.params.path || ''];
}

if (integrations?.polylang?.enable && (ctx.locale || ctx.defaultLocale)) {
params.lang = ctx.locale ?? ctx.defaultLocale;
}

const stringPath = convertToPath(path);
const defaultParams = fetchStrategy.getDefaultParams();
const urlParams = fetchStrategy.getParamsFromURL(stringPath, params);

const finalParams = deepmerge.all([defaultParams, urlParams, params]) as Partial<P>;

// we don't want to include the preview params in the key
const key = fetchStrategy.getCacheKey(finalParams);
const { debug, preview } = getSiteFromContext(ctx);

if (debug?.devMode) {
log(LOGTYPE.INFO, `[fetchHookData] key for ${key.url}`, key);
}

if (
isPreviewRequest(finalParams, urlParams) &&
isPreviewRequest(params, urlParams) &&
typeof ctx.preview !== 'undefined' &&
typeof ctx.previewData !== 'undefined'
) {
finalParams.id = ctx.previewData.id;
finalParams.revision = ctx.previewData.revision;
finalParams.postType = ctx.previewData.postType;
finalParams.authToken = ctx.previewData.authToken;
params.id = ctx.previewData.id;
params.revision = ctx.previewData.revision;
params.postType = ctx.previewData.postType;
params.authToken = ctx.previewData.authToken;

if (debug?.requests) {
log(LOGTYPE.DEBUG, 'Preview request detected, using preview data', ctx.previewData);
Expand All @@ -138,24 +166,20 @@ export async function fetchHookData<T = unknown, P extends EndpointParams = Endp
}
}

const data = await fetchStrategy.fetcher(
fetchStrategy.buildEndpointURL(finalParams),
finalParams,
{
// burst cache to skip REST API cache when the request is being made under getStaticProps
// if .req is not available then this is a GetStaticPropsContext
burstCache: typeof (ctx as GetServerSidePropsContext).req === 'undefined',
...options.fetchStrategyOptions,
},
);
const data = await fetchStrategy.fetcher(fetchStrategy.buildEndpointURL(params), params, {
// burst cache to skip REST API cache when the request is being made under getStaticProps
// if .req is not available then this is a GetStaticPropsContext
burstCache: typeof (ctx as GetServerSidePropsContext).req === 'undefined',
...options.fetchStrategyOptions,
});

if (debug?.devMode) {
log(LOGTYPE.INFO, `[fetchHookData] data.pageInfo for ${key.url}`, data.pageInfo);
}

const normalizedData = fetchStrategy.normalizeForCache(
fetchStrategy.filterData(data, options.filterData as unknown as FilterDataOptions<R>),
finalParams,
params,
);

let additionalCacheObjects;
Expand All @@ -164,14 +188,14 @@ export async function fetchHookData<T = unknown, P extends EndpointParams = Endp
additionalCacheObjects = normalizedData.additionalCacheObjects.map((cacheObject) => ({
...cacheObject,
key: serializeKey(cacheObject.key),
isMainQuery: fetchStrategy.isMainQuery(stringPath, params),
isMainQuery: fetchStrategy.isMainQuery(path, params),
}));
}

return {
...normalizedData,
key: serializeKey(key),
isMainQuery: fetchStrategy.isMainQuery(stringPath, params),
isMainQuery: fetchStrategy.isMainQuery(path, params),
additionalCacheObjects: additionalCacheObjects || null,
};
}

0 comments on commit 8ffb944

Please sign in to comment.