Skip to content
This repository was archived by the owner on Jan 19, 2026. It is now read-only.

Commit 13dbb55

Browse files
authored
fix: add pagination control to custom-fetch and fetchStories method (#222)
* fix: add pagination control to custom-fetch and fetchStories method * fix(stories): setting default per_page parameter to 100 * fix(tests): update fetchStories tests to include default per_page parameter in request URLs
1 parent 5b93c9c commit 13dbb55

7 files changed

Lines changed: 117 additions & 41 deletions

File tree

src/commands/login/actions.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ afterAll(() => server.close());
4343
describe('login actions', () => {
4444
describe('loginWithToken', () => {
4545
it('should login successfully with a valid token', async () => {
46-
const mockResponse = { data: 'user data' };
46+
const mockResponse = { data: 'user data', perPage: 0, total: 0 };
4747
const result = await loginWithToken('valid-token', 'eu');
4848
expect(result).toEqual(mockResponse);
4949
});
@@ -70,7 +70,7 @@ describe('login actions', () => {
7070

7171
describe('loginWithEmailAndPassword', () => {
7272
it('should get if the user requires otp', async () => {
73-
const expected = { otp_required: true };
73+
const expected = { otp_required: true, perPage: 0, total: 0 };
7474
const result = await loginWithEmailAndPassword('julio.iglesias@storyblok.com', 'password', 'eu');
7575
expect(result).toEqual(expected);
7676
});
@@ -96,7 +96,7 @@ describe('login actions', () => {
9696
}
9797
}),
9898
);
99-
const expected = { access_token: 'Awiwi' };
99+
const expected = { access_token: 'Awiwi', perPage: 0, total: 0 };
100100

101101
const result = await loginWithOtp('julio.iglesias@storyblok.com', 'password', '123456', 'eu');
102102

src/commands/stories/actions.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,35 +272,35 @@ describe('stories/actions', () => {
272272

273273
it('should fetch stories without filters', async () => {
274274
await fetchStoriesByComponent(mockSpaceOptions);
275-
expect(requestUrl).toBe('');
275+
expect(requestUrl).toBe('?per_page=100');
276276
});
277277

278278
it('should fetch stories with component filter', async () => {
279279
await fetchStoriesByComponent(mockSpaceOptions, {
280280
componentName: 'test-component',
281281
});
282-
expect(requestUrl).toBe('?contain_component=test-component');
282+
expect(requestUrl).toBe('?contain_component=test-component&per_page=100');
283283
});
284284

285285
it('should fetch stories with starts_with filter', async () => {
286286
await fetchStoriesByComponent(mockSpaceOptions, {
287287
starts_with: '/en/blog/',
288288
});
289-
expect(requestUrl).toBe('?starts_with=%2Fen%2Fblog%2F');
289+
expect(requestUrl).toBe('?starts_with=%2Fen%2Fblog%2F&per_page=100');
290290
});
291291

292292
it('should fetch stories with filter_query parameter', async () => {
293293
await fetchStoriesByComponent(mockSpaceOptions, {
294294
query: '[highlighted][is]=true',
295295
});
296-
expect(requestUrl).toBe('?filter_query[highlighted][is]=true');
296+
expect(requestUrl).toBe('?per_page=100&filter_query[highlighted][is]=true');
297297
});
298298

299299
it('should handle already prefixed filter_query parameter', async () => {
300300
await fetchStoriesByComponent(mockSpaceOptions, {
301301
query: 'filter_query[highlighted][is]=true',
302302
});
303-
expect(requestUrl).toBe('?filter_query[highlighted][is]=true');
303+
expect(requestUrl).toBe('?per_page=100&filter_query[highlighted][is]=true');
304304
});
305305

306306
it('should handle multiple filters together', async () => {
@@ -309,7 +309,7 @@ describe('stories/actions', () => {
309309
starts_with: '/en/blog/',
310310
query: '[highlighted][is]=true',
311311
});
312-
expect(requestUrl).toBe('?starts_with=%2Fen%2Fblog%2F&contain_component=test-component&filter_query[highlighted][is]=true');
312+
expect(requestUrl).toBe('?starts_with=%2Fen%2Fblog%2F&contain_component=test-component&per_page=100&filter_query[highlighted][is]=true');
313313
});
314314

315315
it('should handle error responses', async () => {

src/commands/stories/actions.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,46 @@ export const fetchStories = async (
2121
) => {
2222
try {
2323
const url = getStoryblokUrl(region);
24-
25-
// Extract filter_query params to handle them separately
26-
const { filter_query, ...restParams } = params || {};
27-
28-
// Handle regular params with URLSearchParams
29-
const regularParams = new URLSearchParams(objectToStringParams(restParams)).toString();
30-
31-
// Combine regular params with filter_query params (if any)
32-
const queryString = filter_query
33-
? `${regularParams ? `${regularParams}&` : ''}${filter_query}`
34-
: regularParams;
35-
36-
const endpoint = `${url}/spaces/${space}/stories${queryString ? `?${queryString}` : ''}`;
37-
38-
const response = await customFetch<{
39-
stories: Story[];
40-
}>(endpoint, {
41-
headers: {
42-
Authorization: token,
43-
},
44-
});
45-
return response.stories;
24+
const allStories: Story[] = [];
25+
let currentPage = 1;
26+
let hasMorePages = true;
27+
28+
while (hasMorePages) {
29+
// Extract filter_query params to handle them separately
30+
const { filter_query, ...restParams } = params || {};
31+
32+
// Handle regular params with URLSearchParams
33+
const regularParams = new URLSearchParams({
34+
...objectToStringParams({ ...restParams, per_page: 100 }),
35+
...(currentPage > 1 && { page: currentPage.toString() }),
36+
}).toString();
37+
38+
// Combine regular params with filter_query params (if any)
39+
const queryString = filter_query
40+
? `${regularParams ? `${regularParams}&` : ''}${filter_query}`
41+
: regularParams;
42+
43+
const endpoint = `${url}/spaces/${space}/stories${queryString ? `?${queryString}` : ''}`;
44+
45+
const response = await customFetch<{
46+
stories: Story[];
47+
per_page: number;
48+
total: number;
49+
}>(endpoint, {
50+
headers: {
51+
Authorization: token,
52+
},
53+
});
54+
55+
allStories.push(...response.stories);
56+
57+
// Check if we have more pages to fetch
58+
const totalPages = Math.ceil(response.total / response.perPage);
59+
hasMorePages = currentPage < totalPages;
60+
currentPage++;
61+
}
62+
63+
return allStories;
4664
}
4765
catch (error) {
4866
handleAPIError('pull_stories', error as Error);

src/commands/user/actions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('user actions', () => {
2929

3030
describe('getUser', () => {
3131
it('should get user successfully with a valid token', async () => {
32-
const mockResponse = { data: 'user data' };
32+
const mockResponse = { data: 'user data', perPage: 0, total: 0 };
3333
const result = await getUser('valid-token', 'eu');
3434
expect(result).toEqual(mockResponse);
3535
});

src/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import './commands/types';
1313
import pkg from '../package.json';
1414

1515
import { colorPalette } from './constants';
16+
import { session } from './session';
17+
import { fetchStories } from './commands/stories';
1618

1719
export * from './types/storyblok';
1820

@@ -33,6 +35,25 @@ program.on('command:*', () => {
3335
program.help();
3436
});
3537

38+
program.command('test')
39+
.description('Test the CLI')
40+
.action(async () => {
41+
const { state, initializeSession } = session();
42+
await initializeSession();
43+
44+
const { password, region } = state;
45+
46+
try {
47+
const result = await fetchStories('85047', password, region, {
48+
per_page: 100,
49+
});
50+
console.log(result?.length);
51+
}
52+
catch (error) {
53+
console.error(error);
54+
}
55+
});
56+
3657
try {
3758
program.parse(process.argv);
3859
}

src/utils/fetch.test.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,31 @@ describe('customFetch', () => {
1010
const mockResponse = { data: 'test' };
1111
mockFetch.mockResolvedValueOnce({
1212
ok: true,
13-
headers: { get: () => 'application/json' },
13+
headers: new Headers({
14+
'content-type': 'application/json',
15+
'per-page': '10',
16+
'total': '100',
17+
}),
1418
json: () => Promise.resolve(mockResponse),
1519
});
1620

1721
const result = await customFetch('https://api.test.com');
18-
expect(result).toEqual(mockResponse);
22+
expect(result).toEqual({
23+
...mockResponse,
24+
perPage: 10,
25+
total: 100,
26+
});
1927
});
2028

2129
it('should handle object body by stringifying it', async () => {
2230
const body = { test: 'data' };
2331
mockFetch.mockResolvedValueOnce({
2432
ok: true,
25-
headers: { get: () => 'application/json' },
33+
headers: new Headers({
34+
'content-type': 'application/json',
35+
'per-page': '10',
36+
'total': '100',
37+
}),
2638
json: () => Promise.resolve({}),
2739
});
2840

@@ -37,7 +49,11 @@ describe('customFetch', () => {
3749
const body = '{"test":"data"}';
3850
mockFetch.mockResolvedValueOnce({
3951
ok: true,
40-
headers: { get: () => 'application/json' },
52+
headers: new Headers({
53+
'content-type': 'application/json',
54+
'per-page': '10',
55+
'total': '100',
56+
}),
4157
json: () => Promise.resolve({}),
4258
});
4359

@@ -52,7 +68,11 @@ describe('customFetch', () => {
5268
const body = ['test', 'data'];
5369
mockFetch.mockResolvedValueOnce({
5470
ok: true,
55-
headers: { get: () => 'application/json' },
71+
headers: new Headers({
72+
'content-type': 'application/json',
73+
'per-page': '10',
74+
'total': '100',
75+
}),
5676
json: () => Promise.resolve({}),
5777
});
5878

@@ -67,7 +87,11 @@ describe('customFetch', () => {
6787
const textResponse = 'Hello World';
6888
mockFetch.mockResolvedValueOnce({
6989
ok: true,
70-
headers: { get: () => 'text/plain' },
90+
headers: new Headers({
91+
'content-type': 'text/plain',
92+
'per-page': '10',
93+
'total': '100',
94+
}),
7195
text: () => Promise.resolve(textResponse),
7296
});
7397

@@ -80,6 +104,11 @@ describe('customFetch', () => {
80104
ok: false,
81105
status: 404,
82106
statusText: 'Not Found',
107+
headers: new Headers({
108+
'content-type': 'application/json',
109+
'per-page': '10',
110+
'total': '100',
111+
}),
83112
json: () => Promise.resolve(errorResponse),
84113
});
85114

@@ -109,7 +138,11 @@ describe('customFetch', () => {
109138
it('should set correct headers', async () => {
110139
mockFetch.mockResolvedValueOnce({
111140
ok: true,
112-
headers: { get: () => 'application/json' },
141+
headers: new Headers({
142+
'content-type': 'application/json',
143+
'per-page': '10',
144+
'total': '100',
145+
}),
113146
json: () => Promise.resolve({}),
114147
});
115148

src/utils/fetch.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface FetchOptions {
2222
baseDelay?: number;
2323
}
2424

25-
export async function customFetch<T>(url: string, options: FetchOptions = {}): Promise<T> {
25+
export async function customFetch<T>(url: string, options: FetchOptions = {}): Promise<T & { perPage: number; total: number }> {
2626
const maxRetries = options.maxRetries ?? 3;
2727
const baseDelay = options.baseDelay ?? 500; // 500ms base delay
2828
let attempt = 0;
@@ -77,7 +77,11 @@ export async function customFetch<T>(url: string, options: FetchOptions = {}): P
7777
});
7878
}
7979

80-
return data;
80+
return {
81+
...data,
82+
perPage: Number(response.headers.get('Per-Page')),
83+
total: Number(response.headers.get('Total')),
84+
};
8185
}
8286
catch (error) {
8387
if (error instanceof FetchError) {

0 commit comments

Comments
 (0)