Skip to content

Commit cb49df7

Browse files
committed
feat: accept a custom fetcher function in all methods
1 parent 06e899f commit cb49df7

File tree

14 files changed

+235
-268
lines changed

14 files changed

+235
-268
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
"analyze": "size-limit --why"
2727
},
2828
"dependencies": {
29-
"cross-fetch": "^3.1.5",
3029
"humps": "^2.0.1"
3130
},
3231
"devDependencies": {
32+
"cross-fetch": "^3.1.5",
3333
"@size-limit/preset-small-lib": "^7.0.8",
3434
"@types/humps": "^2.0.1",
3535
"@typescript-eslint/eslint-plugin": "^5.42.0",
@@ -62,4 +62,4 @@
6262
"engines": {
6363
"node": ">=12"
6464
}
65-
}
65+
}

src/sdk/v4/_fetcher.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import fetch from 'cross-fetch';
2-
import { camelizeKeys, decamelizeKeys } from 'humps';
3-
import stringify from '../../utils/qs-stringify';
1+
import { camelizeKeys, decamelize, decamelizeKeys } from 'humps';
2+
import { FetchFn } from '../../types';
3+
import { BaseApiOptions } from '../../types/BaseApiOptions';
44
import { removeBeginningSlash } from '../../utils/misc';
55

66
export const API_BASE_URL = 'https://api.quran.com/api/v4/';
@@ -9,20 +9,71 @@ export const makeUrl = (url: string, params?: Record<string, unknown>) => {
99
const baseUrl = `${API_BASE_URL}${removeBeginningSlash(url)}`;
1010
if (!params) return baseUrl;
1111

12-
const decamelizedKeys = decamelizeKeys(params);
13-
const paramsString = stringify(decamelizedKeys);
12+
const paramsWithDecamelizedKeys = decamelizeKeys(params) as Record<
13+
string,
14+
string
15+
>;
16+
const paramsString = new URLSearchParams(
17+
Object.entries(paramsWithDecamelizedKeys).filter(
18+
([, value]) => value !== undefined
19+
)
20+
).toString();
1421
if (!paramsString) return baseUrl;
1522

1623
return `${baseUrl}?${paramsString}`;
1724
};
1825

1926
export const fetcher = async <T extends object>(
2027
url: string,
21-
params: Record<string, unknown> = {}
28+
params: Record<string, unknown> = {},
29+
fetchFn?: FetchFn
2230
) => {
31+
if (fetchFn) {
32+
const json = await fetchFn(makeUrl(url, params));
33+
return camelizeKeys(json) as T;
34+
}
35+
36+
if (typeof fetch === 'undefined') {
37+
throw new Error('fetch is not defined');
38+
}
39+
40+
// if there is no fetchFn, we use the global fetch
2341
const res = await fetch(makeUrl(url, params));
24-
if (res.status >= 400) throw new Error(`${res.status} ${res.statusText}`);
42+
43+
if (!res.ok || res.status >= 400) {
44+
throw new Error(`${res.status} ${res.statusText}`);
45+
}
46+
2547
const json = await res.json();
2648

2749
return camelizeKeys(json) as T;
2850
};
51+
52+
type MergeApiOptionsObject = Pick<BaseApiOptions, 'fetchFn'> & {
53+
fields?: Record<string, boolean>;
54+
} & Record<string, unknown>;
55+
56+
export const mergeApiOptions = (
57+
options: MergeApiOptionsObject = {},
58+
defaultOptions: Record<string, unknown> = {}
59+
) => {
60+
// we can set it to undefined because `makeUrl` will filter it out
61+
if (options.fetchFn) options.fetchFn = undefined;
62+
63+
const final: Record<string, unknown> = {
64+
...defaultOptions,
65+
...options,
66+
};
67+
68+
if (final.fields) {
69+
const fields: string[] = [];
70+
Object.entries(final.fields).forEach(([key, value]) => {
71+
if (value) fields.push(decamelize(key));
72+
});
73+
74+
// convert `fields` to a string sperated by commas
75+
final.fields = fields.join(',');
76+
}
77+
78+
return final;
79+
};

src/sdk/v4/audio.ts

Lines changed: 30 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,52 +11,26 @@ import {
1111
VerseKey,
1212
VerseRecitationField,
1313
} from '../../types';
14-
import { decamelize } from 'humps';
1514
import Utils from '../utils';
16-
import { fetcher } from './_fetcher';
15+
import { fetcher, mergeApiOptions } from './_fetcher';
16+
import { BaseApiOptions } from '../../types/BaseApiOptions';
1717

18-
type GetChapterRecitationOptions = Partial<{
19-
language: Language;
20-
}>;
18+
type GetChapterRecitationOptions = Partial<BaseApiOptions>;
2119

2220
const defaultChapterRecitationsOptions: GetChapterRecitationOptions = {
2321
language: Language.ARABIC,
2422
};
2523

26-
const getChapterRecitationsOptions = (
27-
options: GetChapterRecitationOptions = {}
28-
) => {
29-
const final: any = { ...defaultChapterRecitationsOptions, ...options };
30-
31-
return final;
32-
};
33-
34-
type GetVerseRecitationOptions = Partial<{
35-
language: Language;
36-
fields: Partial<Record<VerseRecitationField, boolean>>;
37-
}>;
24+
type GetVerseRecitationOptions = Partial<
25+
BaseApiOptions & {
26+
fields: Partial<Record<VerseRecitationField, boolean>>;
27+
}
28+
>;
3829

3930
const defaultVerseRecitationsOptions: GetVerseRecitationOptions = {
4031
language: Language.ARABIC,
4132
};
4233

43-
const getVerseRecitationsOptions = (
44-
options: GetVerseRecitationOptions = {}
45-
) => {
46-
const initial = { ...defaultVerseRecitationsOptions, ...options };
47-
const final: any = { language: initial.language };
48-
49-
if (initial.fields) {
50-
const fields: string[] = [];
51-
for (const [key, value] of Object.entries(initial.fields)) {
52-
if (value) fields.push(decamelize(key));
53-
}
54-
final.fields = fields.join(',');
55-
}
56-
57-
return final;
58-
};
59-
6034
/**
6135
* Get all chapter recitations for specific reciter
6236
* @description https://quran.api-docs.io/v4/audio-recitations/list-of-all-surah-audio-files-for-specific-reciter
@@ -69,10 +43,11 @@ const findAllChapterRecitations = async (
6943
reciterId: string,
7044
options?: GetChapterRecitationOptions
7145
) => {
72-
const params = getChapterRecitationsOptions(options);
46+
const params = mergeApiOptions(options, defaultChapterRecitationsOptions);
7347
const { audioFiles } = await fetcher<{ audioFiles: ChapterRecitation[] }>(
7448
`/chapter_recitations/${reciterId}`,
75-
params
49+
params,
50+
options?.fetchFn
7651
);
7752
return audioFiles;
7853
};
@@ -93,10 +68,11 @@ const findChapterRecitationById = async (
9368
) => {
9469
if (!Utils.isValidChapterId(chapterId)) throw new Error('Invalid chapter id');
9570

96-
const params = getChapterRecitationsOptions(options);
71+
const params = mergeApiOptions(options, defaultChapterRecitationsOptions);
9772
const { audioFile } = await fetcher<{ audioFile: ChapterRecitation }>(
9873
`/chapter_recitations/${reciterId}/${chapterId}`,
99-
params
74+
params,
75+
options?.fetchFn
10076
);
10177

10278
return audioFile;
@@ -118,11 +94,15 @@ const findVerseRecitationsByChapter = async (
11894
) => {
11995
if (!Utils.isValidChapterId(chapterId)) throw new Error('Invalid chapter id');
12096

121-
const params = getVerseRecitationsOptions(options);
97+
const params = mergeApiOptions(options, defaultVerseRecitationsOptions);
12298
const data = await fetcher<{
12399
audioFiles: VerseRecitation[];
124100
pagination: Pagination;
125-
}>(`/recitations/${recitationId}/by_chapter/${chapterId}`, params);
101+
}>(
102+
`/recitations/${recitationId}/by_chapter/${chapterId}`,
103+
params,
104+
options?.fetchFn
105+
);
126106

127107
return data;
128108
};
@@ -143,11 +123,11 @@ const findVerseRecitationsByJuz = async (
143123
) => {
144124
if (!Utils.isValidJuz(juz)) throw new Error('Invalid juz');
145125

146-
const params = getVerseRecitationsOptions(options);
126+
const params = mergeApiOptions(options, defaultVerseRecitationsOptions);
147127
const data = await fetcher<{
148128
audioFiles: VerseRecitation[];
149129
pagination: Pagination;
150-
}>(`/recitations/${recitationId}/by_juz/${juz}`, params);
130+
}>(`/recitations/${recitationId}/by_juz/${juz}`, params, options?.fetchFn);
151131

152132
return data;
153133
};
@@ -168,11 +148,11 @@ const findVerseRecitationsByPage = async (
168148
) => {
169149
if (!Utils.isValidQuranPage(page)) throw new Error('Invalid page');
170150

171-
const params = getVerseRecitationsOptions(options);
151+
const params = mergeApiOptions(options, defaultVerseRecitationsOptions);
172152
const data = await fetcher<{
173153
audioFiles: VerseRecitation[];
174154
pagination: Pagination;
175-
}>(`/recitations/${recitationId}/by_page/${page}`, params);
155+
}>(`/recitations/${recitationId}/by_page/${page}`, params, options?.fetchFn);
176156

177157
return data;
178158
};
@@ -193,11 +173,11 @@ const findVerseRecitationsByRub = async (
193173
) => {
194174
if (!Utils.isValidRub(rub)) throw new Error('Invalid rub');
195175

196-
const params = getVerseRecitationsOptions(options);
176+
const params = mergeApiOptions(options, defaultVerseRecitationsOptions);
197177
const data = await fetcher<{
198178
audioFiles: VerseRecitation[];
199179
pagination: Pagination;
200-
}>(`/recitations/${recitationId}/by_rub/${rub}`, params);
180+
}>(`/recitations/${recitationId}/by_rub/${rub}`, params, options?.fetchFn);
201181

202182
return data;
203183
};
@@ -218,11 +198,11 @@ const findVerseRecitationsByHizb = async (
218198
) => {
219199
if (!Utils.isValidHizb(hizb)) throw new Error('Invalid hizb');
220200

221-
const params = getVerseRecitationsOptions(options);
201+
const params = mergeApiOptions(options, defaultVerseRecitationsOptions);
222202
const data = await fetcher<{
223203
audioFiles: VerseRecitation[];
224204
pagination: Pagination;
225-
}>(`/recitations/${recitationId}/by_hizb/${hizb}`, params);
205+
}>(`/recitations/${recitationId}/by_hizb/${hizb}`, params, options?.fetchFn);
226206

227207
return data;
228208
};
@@ -243,11 +223,11 @@ const findVerseRecitationsByKey = async (
243223
) => {
244224
if (!Utils.isValidVerseKey(key)) throw new Error('Invalid verse key');
245225

246-
const params = getVerseRecitationsOptions(options);
226+
const params = mergeApiOptions(options, defaultVerseRecitationsOptions);
247227
const data = await fetcher<{
248228
audioFiles: VerseRecitation[];
249229
pagination: Pagination;
250-
}>(`/recitations/${recitationId}/by_ayah/${key}`, params);
230+
}>(`/recitations/${recitationId}/by_ayah/${key}`, params, options?.fetchFn);
251231

252232
return data;
253233
};

src/sdk/v4/chapters.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
import { Chapter, ChapterId, ChapterInfo, Language } from '../../types';
2-
import { fetcher } from './_fetcher';
2+
import { fetcher, mergeApiOptions } from './_fetcher';
33
import Utils from '../utils';
4+
import { BaseApiOptions } from '../../types/BaseApiOptions';
45

5-
type GetChapterOptions = Partial<{
6-
language: Language;
7-
}>;
6+
type GetChapterOptions = Partial<BaseApiOptions>;
87

98
const defaultOptions: GetChapterOptions = {
109
language: Language.ARABIC,
1110
};
1211

13-
const getChapterOptions = (options: GetChapterOptions = {}) => {
14-
const final: any = { ...defaultOptions, ...options };
15-
return final;
16-
};
17-
1812
/**
1913
* Get all chapters.
2014
* @description https://quran.api-docs.io/v4/chapters/list-chapters
@@ -23,10 +17,11 @@ const getChapterOptions = (options: GetChapterOptions = {}) => {
2317
* quran.v4.chapters.findAll()
2418
*/
2519
const findAll = async (options?: GetChapterOptions) => {
26-
const params = getChapterOptions(options);
20+
const params = mergeApiOptions(options, defaultOptions);
2721
const { chapters } = await fetcher<{ chapters: Chapter[] }>(
2822
'/chapters',
29-
params
23+
params,
24+
options?.fetchFn
3025
);
3126

3227
return chapters;
@@ -44,10 +39,11 @@ const findAll = async (options?: GetChapterOptions) => {
4439
const findById = async (id: ChapterId, options?: GetChapterOptions) => {
4540
if (!Utils.isValidChapterId(id)) throw new Error('Invalid chapter id');
4641

47-
const params = getChapterOptions(options);
42+
const params = mergeApiOptions(options, defaultOptions);
4843
const { chapter } = await fetcher<{ chapter: Chapter }>(
4944
`/chapters/${id}`,
50-
params
45+
params,
46+
options?.fetchFn
5147
);
5248

5349
return chapter;
@@ -65,10 +61,11 @@ const findById = async (id: ChapterId, options?: GetChapterOptions) => {
6561
const findInfoById = async (id: ChapterId, options?: GetChapterOptions) => {
6662
if (!Utils.isValidChapterId(id)) throw new Error('Invalid chapter id');
6763

68-
const params = getChapterOptions(options);
64+
const params = mergeApiOptions(options, defaultOptions);
6965
const { chapterInfo } = await fetcher<{ chapterInfo: ChapterInfo }>(
7066
`/chapters/${id}/info`,
71-
params
67+
params,
68+
options?.fetchFn
7269
);
7370

7471
return chapterInfo;

src/sdk/v4/juzs.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Juz } from '../../types';
2+
import { BaseApiOptions } from '../../types/BaseApiOptions';
23
import { fetcher } from './_fetcher';
34

45
/**
@@ -7,8 +8,12 @@ import { fetcher } from './_fetcher';
78
* @example
89
* quran.v4.juzs.findAll()
910
*/
10-
const findAll = async () => {
11-
const { juzs } = await fetcher<{ juzs: Juz[] }>('/juzs');
11+
const findAll = async (options?: Omit<BaseApiOptions, 'language'>) => {
12+
const { juzs } = await fetcher<{ juzs: Juz[] }>(
13+
'/juzs',
14+
undefined,
15+
options?.fetchFn
16+
);
1217
return juzs;
1318
};
1419

0 commit comments

Comments
 (0)