Skip to content

Commit 201dc39

Browse files
authored
Merge pull request #12 from quran/dev
v1.7.0
2 parents a5b5f84 + 0b8c657 commit 201dc39

File tree

17 files changed

+294
-278
lines changed

17 files changed

+294
-278
lines changed

docs/src/pages/_meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"index": "Introduction",
3+
"techniques": "Techniques",
34
"chapters": "Chapters",
45
"verses": "Verses",
56
"search": "Search",
67
"juzs": "Juzs",
78
"audio": "Audio",
89
"resources": "Resources",
910
"utils": "Utilities"
10-
}
11+
}

docs/src/pages/techniques.mdx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## Custom fetcher
2+
3+
By default, all functions that interact with the [Quran.com API](https://quran.api-docs.io/v4) use the global `fetch` function.
4+
5+
You can override this by passing a custom fetcher (as `fetchFn`) to the options object of any method.
6+
7+
import { Callout } from 'nextra-theme-docs';
8+
9+
<Callout>
10+
Note that the `fetchFn` accepts a string url and must return a promise with
11+
the JSON response.
12+
</Callout>
13+
14+
### Examples
15+
16+
#### Axios
17+
18+
```ts
19+
import axios from 'axios';
20+
21+
const chapters = await quran.v4.chapters.findAll({
22+
fetchFn: (url) => axios.get(url).then((res) => res.data),
23+
});
24+
```
25+
26+
---
27+
28+
#### Node-fetch
29+
30+
```ts
31+
import fetch from 'node-fetch';
32+
33+
const chapters = await quran.v4.chapters.findAll({
34+
fetchFn: async (url) => {
35+
const response = await fetch(url);
36+
return response.json();
37+
},
38+
});
39+
```

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/utils.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import {
2+
ChapterId,
3+
HizbNumber,
4+
JuzNumber,
5+
PageNumber,
6+
RubNumber,
7+
VerseKey,
8+
} from '../types';
9+
110
// this maps chapterNumber to verseCount
211
export const versesMapping = {
312
'1': 7,
@@ -126,7 +135,7 @@ export const versesMapping = {
126135
isValidChapterId('-1') // false
127136
isValidChapterId('200') // false
128137
*/
129-
const isValidChapterId = (id: string | number) => {
138+
const isValidChapterId = (id: string | number): id is ChapterId => {
130139
const parsedId = typeof id === 'number' ? id : Number(id);
131140
if (!parsedId || parsedId <= 0 || parsedId > 114) return false;
132141
return true;
@@ -142,7 +151,7 @@ const isValidChapterId = (id: string | number) => {
142151
isValidJuz('-1') // false
143152
isValidJuz('200') // false
144153
*/
145-
const isValidJuz = (juz: string | number) => {
154+
const isValidJuz = (juz: string | number): juz is JuzNumber => {
146155
const parsedJuz = typeof juz === 'number' ? juz : Number(juz);
147156
if (!parsedJuz || parsedJuz <= 0 || parsedJuz > 30) return false;
148157
return true;
@@ -158,7 +167,7 @@ const isValidJuz = (juz: string | number) => {
158167
isValidRub('-1') // false
159168
isValidRub('300') // false
160169
*/
161-
const isValidRub = (rub: string | number) => {
170+
const isValidRub = (rub: string | number): rub is RubNumber => {
162171
const parsedRub = typeof rub === 'number' ? rub : Number(rub);
163172
if (!parsedRub || parsedRub <= 0 || parsedRub > 240) return false;
164173
return true;
@@ -174,7 +183,7 @@ const isValidRub = (rub: string | number) => {
174183
isValidHizb('-1') // false
175184
isValidHizb('200') // false
176185
*/
177-
const isValidHizb = (hizb: string | number) => {
186+
const isValidHizb = (hizb: string | number): hizb is HizbNumber => {
178187
const parsedHizb = typeof hizb === 'number' ? hizb : Number(hizb);
179188
if (!parsedHizb || parsedHizb <= 0 || parsedHizb > 60) return false;
180189
return true;
@@ -190,7 +199,7 @@ const isValidHizb = (hizb: string | number) => {
190199
isValidQuranPage('-1') // false
191200
isValidQuranPage('1000') // false
192201
*/
193-
const isValidQuranPage = (page: string | number) => {
202+
const isValidQuranPage = (page: string | number): page is PageNumber => {
194203
const parsedPage = typeof page === 'number' ? page : Number(page);
195204
if (!parsedPage || parsedPage <= 0 || parsedPage > 604) return false;
196205
return true;
@@ -206,19 +215,19 @@ const isValidQuranPage = (page: string | number) => {
206215
isValidVerseKey('1:-') // false
207216
isValidVerseKey('1_1') // false
208217
*/
209-
const isValidVerseKey = (key: string) => {
218+
const isValidVerseKey = (key: string): key is VerseKey => {
210219
const [chapterId, verseId] = key.trim().split(':');
211220
if (!chapterId || !verseId || !isValidChapterId(chapterId)) return false;
212221

213222
const parsedVerse = Number(verseId);
214-
const verseCount = (versesMapping as any)[chapterId];
223+
const verseCount = (versesMapping as Record<string, number>)[chapterId];
215224
if (!parsedVerse || parsedVerse <= 0 || parsedVerse > verseCount)
216225
return false;
217226

218227
return true;
219228
};
220229

221-
const Utils = {
230+
const utils = {
222231
isValidChapterId,
223232
isValidJuz,
224233
isValidRub,
@@ -227,4 +236,4 @@ const Utils = {
227236
isValidVerseKey,
228237
};
229238

230-
export default Utils;
239+
export default utils;

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+
};

0 commit comments

Comments
 (0)