Skip to content

Commit ee9dcb3

Browse files
committed
refactor(type): add type aware getAll function that works with ISbLinksParams
Problem: - Developers may need to `getAll` when getting an unpaginated response from `cdn/links` - Currently when calling `cdn/links` with ISbLinksParams results with compile time error when using ISbLinksParams specific params like: include_dates Solution: - Make `getAll` function generic to make it type aware of the intention of the developer - Updated the function to not allow developers to modify the number of round trips for fetching all results - Updated unit tests and e2e tests Closes: storyblok#944
1 parent de79caf commit ee9dcb3

File tree

4 files changed

+101
-43
lines changed

4 files changed

+101
-43
lines changed

src/index.test.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -536,18 +536,18 @@ describe('storyblokClient', () => {
536536
});
537537

538538
it('should make a request for each page', async () => {
539+
const total = 1042;
540+
const links = Array.from({ length: total }, (_, i) => ({ id: i, name: `Test ${i}` }));
539541
const mockMakeRequest = vi.fn().mockResolvedValue({
540542
data: {
541-
links: [
542-
{ id: 1, name: 'Test 1' },
543-
{ id: 2, name: 'Test 2' },
544-
],
543+
links,
545544
},
546-
total: 2,
545+
total,
547546
status: 200,
548547
});
549548
client.makeRequest = mockMakeRequest;
550-
await client.getAll('links', { per_page: 1 });
549+
550+
await client.getAll('links');
551551
expect(mockMakeRequest).toBeCalledTimes(2);
552552
});
553553

src/index.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
ISbConfig,
1313
ISbContentMangmntAPI,
1414
ISbCustomFetch,
15+
ISbLink,
1516
ISbLinksParams,
1617
ISbLinksResult,
1718
ISbLinkURLObject,
@@ -215,7 +216,7 @@ class Storyblok {
215216

216217
private makeRequest(
217218
url: string,
218-
params: ISbStoriesParams,
219+
params: ISbStoriesParams | ISbLinksParams,
219220
per_page: number,
220221
page: number,
221222
fetchOptions?: ISbCustomFetch,
@@ -254,30 +255,35 @@ class Storyblok {
254255
return this.cacheResponse(url, query, undefined, fetchOptions);
255256
}
256257

257-
public async getAll(
258-
slug: string,
259-
params: ISbStoriesParams,
258+
public async getAll<T extends string>(
259+
slug: T,
260+
params?: T extends 'cdn/links' ? Omit<ISbLinksParams, 'per_page'
261+
| 'paginated' | 'page'> : Omit<ISbStoriesParams, 'per_page' | 'page'>,
260262
entity?: string,
261263
fetchOptions?: ISbCustomFetch,
262-
): Promise<any[]> {
263-
const perPage = params?.per_page || 25;
264+
): Promise<T extends 'cdn/links' ? ISbLink[] : any[]> {
265+
const maxPerPage = 1000; // Max allowed per page response from a paginated API
266+
const requestParams = {
267+
...(params ?? {}),
268+
perPage: maxPerPage,
269+
};
264270
const url = `/${slug}`.replace(/\/$/, '');
265271
const e = entity ?? url.substring(url.lastIndexOf('/') + 1);
266272

267273
const firstPage = 1;
268274
const firstRes = await this.makeRequest(
269275
url,
270-
params,
271-
perPage,
276+
requestParams,
277+
maxPerPage,
272278
firstPage,
273279
fetchOptions,
274280
);
275-
const lastPage = firstRes.total ? Math.ceil(firstRes.total / perPage) : 1;
281+
const lastPage = firstRes.total ? Math.ceil(firstRes.total / maxPerPage) : 1;
276282

277283
const restRes: any = await this.helpers.asyncMap(
278284
this.helpers.range(firstPage, lastPage),
279285
(i: number) => {
280-
return this.makeRequest(url, params, perPage, i + 1, fetchOptions);
286+
return this.makeRequest(url, requestParams, maxPerPage, i + 1, fetchOptions);
281287
},
282288
);
283289

src/sbHelpers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
AsyncFn,
33
HtmlEscapes,
4+
ISbLinksParams,
45
ISbResult,
56
ISbStoriesParams,
67
} from './interfaces';
@@ -19,7 +20,7 @@ export class SbHelpers {
1920
public isCDNUrl = (url = '') => url.includes('/cdn/');
2021

2122
public getOptionsPage = (
22-
options: ISbStoriesParams,
23+
options: ISbStoriesParams | ISbLinksParams,
2324
perPage = 25,
2425
page = 1,
2526
) => {

tests/api/index.e2e.ts

+77-26
Original file line numberDiff line numberDiff line change
@@ -85,40 +85,91 @@ describe('StoryblokClient', () => {
8585
});
8686

8787
describe('getAll function', () => {
88-
it('getAll(\'cdn/stories\') should return all stories', async () => {
89-
const result = await client.getAll('cdn/stories', {});
90-
expect(result.length).toBeGreaterThan(0);
91-
});
88+
describe('getAll(\'cdn/stories\')', () => {
89+
it('should not compile if any of `per_page`, or `page` is passed on to the param', async () => {
90+
const result = await client.getAll('cdn/stories', {
91+
// @ts-expect-error the client when calling get all is fully aware that all results will be loaded in memory. This type check assertion ensures that it is not upto the client on how many round trips StoryblokClient will make in order to get all result.
92+
per_page: 20,
93+
page: 1,
94+
});
95+
expect(result.length).toBeGreaterThan(0);
96+
});
9297

93-
it('getAll(\'cdn/stories\') should return all stories with filtered results', async () => {
94-
const result = await client.getAll('cdn/stories', {
95-
starts_with: 'testcontent-0',
98+
it('should return all stories', async () => {
99+
const result = await client.getAll('cdn/stories');
100+
expect(result.length).toBeGreaterThan(0);
96101
});
97-
expect(result.length).toBe(1);
98-
});
99102

100-
it('getAll(\'cdn/stories\', filter_query: { __or: [{ category: { any_in_array: \'Category 1\' } }, { category: { any_in_array: \'Category 2\' } }]}) should return all stories with the specific filter applied', async () => {
101-
const result = await client.getAll('cdn/stories', {
102-
filter_query: {
103-
__or: [
104-
{ category: { any_in_array: 'Category 1' } },
105-
{ category: { any_in_array: 'Category 2' } },
106-
],
107-
},
103+
it('should return all stories with filtered results', async () => {
104+
const result = await client.getAll('cdn/stories', {
105+
starts_with: 'testcontent-0',
106+
});
107+
expect(result.length).toBe(1);
108108
});
109-
expect(result.length).toBeGreaterThan(0);
110-
});
111109

112-
it('getAll(\'cdn/stories\', {by_slugs: \'folder/*\'}) should return all stories with the specific filter applied', async () => {
113-
const result = await client.getAll('cdn/stories', {
114-
by_slugs: 'folder/*',
110+
it('should return all stories with the specific filter applied', async () => {
111+
const result = await client.getAll('cdn/stories', {
112+
filter_query: {
113+
__or: [
114+
{ category: { any_in_array: 'Category 1' } },
115+
{ category: { any_in_array: 'Category 2' } },
116+
],
117+
},
118+
});
119+
expect(result.length).toBeGreaterThan(0);
120+
});
121+
122+
it('should return all stories with the specific filter applied', async () => {
123+
const result = await client.getAll('cdn/stories', {
124+
by_slugs: 'folder/*',
125+
});
126+
expect(result.length).toBeGreaterThan(0);
115127
});
116-
expect(result.length).toBeGreaterThan(0);
117128
});
118129

119-
it('getAll(\'cdn/links\') should return all links', async () => {
120-
const result = await client.getAll('cdn/links', {});
121-
expect(result.length).toBeGreaterThan(0);
130+
describe('getAll(\'cdn/links\')', () => {
131+
it('should not compile if any of `per_page`, `paginated` or `page` is passed on to the param', async () => {
132+
const result = await client.getAll('cdn/links', {
133+
// @ts-expect-error the client when calling get all is fully aware that all results will be loaded in memory. This type check assertion ensures that it is not upto the client on how many round trips StoryblokClient will make in order to get all result.
134+
per_page: 20,
135+
paginated: 1,
136+
page: 1,
137+
});
138+
expect(result.length).toBeGreaterThan(0);
139+
});
140+
141+
it('should return all links', async () => {
142+
const result = await client.getAll('cdn/links');
143+
expect(result.length).toBeGreaterThan(0);
144+
});
145+
146+
it('should return key wise links to be flattened out to an array of ISbLink shape', async () => {
147+
const result = await client.getAll('cdn/links');
148+
const shapeDefinition = {
149+
id: expect.toSatisfy(value => typeof value === 'number' || value === null),
150+
slug: expect.toSatisfy(value => typeof value === 'string' || value === null),
151+
name: expect.toSatisfy(value => typeof value === 'string' || value === null),
152+
is_folder: expect.toSatisfy(value => typeof value === 'boolean' || value === null),
153+
parent_id: expect.toSatisfy(value => typeof value === 'number' || value === null),
154+
published: expect.toSatisfy(value => typeof value === 'boolean' || value === null),
155+
position: expect.toSatisfy(value => typeof value === 'number' || value === null),
156+
uuid: expect.toSatisfy(value => typeof value === 'string' || value === null),
157+
is_startpage: expect.toSatisfy(value => typeof value === 'boolean' || value === null),
158+
path: expect.toSatisfy(value => typeof value === 'string' || value === null),
159+
real_path: expect.toSatisfy(value => typeof value === 'string' || value === null),
160+
};
161+
162+
result.forEach((data) => {
163+
expect(data).toStrictEqual(shapeDefinition);
164+
});
165+
});
166+
167+
it('should return all links with filtered results', async () => {
168+
const result = await client.getAll('cdn/links', {
169+
starts_with: 'testcontent-0',
170+
});
171+
expect(result.length).toBe(1);
172+
});
122173
});
123174
});
124175

0 commit comments

Comments
 (0)