diff --git a/.gitignore b/.gitignore index a887c91..802996c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules dist coverage -.husky \ No newline at end of file +.husky +.env \ No newline at end of file diff --git a/README.md b/README.md index 9d551c5..00f3f4a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ ![Quranjs Api Header](https://github.com/quran/api-js/raw/master/media/repo-header.png) -A library for fetching quran data from the [quran.com API][qdc-api]. This library also works on both Node.js and the browser. -[Checkout Docs][docs] +# Quran Foundation Content API - JavaScript SDK + +This package provides a JavaScript SDK for the **Quran Foundation Content API** and works in both Node.js and the browser. + +> **Full documentation is available at .** [![Build Status][build-badge]][build] [![MIT License][license-badge]][license] @@ -11,26 +14,40 @@ A library for fetching quran data from the [quran.com API][qdc-api]. This librar ## Installation -using npm: - -```ssh +```bash npm install @quranjs/api ``` -using yarn: +or using pnpm / yarn: -```ssh +```bash +pnpm add @quranjs/api +# or yarn add @quranjs/api ``` -## Getting Started +## Quick Start + +```js +import { configure, quran } from '@quranjs/api'; + +configure({ + clientId: '', + clientSecret: '', +}); + +const chapters = await quran.qf.chapters.findAll(); +console.log(chapters); +``` + +For more examples and a complete API reference, see the [SDK documentation](https://api-docs.quran.foundation/sdk). + +## Migrating from previous versions -you can visit the [docs][docs] for more details. +If you used an earlier version of this SDK, please check the migration guide on the [documentation site](https://api-docs.quran.foundation/sdk) for details on upgrading. -[qdc-api]: https://api-docs.quran.com/docs/category/content-apis -[docs]: https://quranjs.com/ [build-badge]: https://github.com/quran/api-js/workflows/CI/badge.svg [build]: https://github.com/quran/api-js/actions?query=workflow%3ACI [license-badge]: https://badgen.net/github/license/quranjs/api diff --git a/package.json b/package.json index 69ccc79..0c0142f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "access": "public" }, "packageManager": "pnpm@9.12.0", - "version": "1.7.1", + "version": "2.0.0", "license": "MIT", "main": "dist/index.min.js", "module": "dist/index.min.mjs", @@ -36,16 +36,21 @@ "analyze": "size-limit --why" }, "dependencies": { + "async-retry": "^1.3.3", + "cross-fetch": "^3.1.5", + "dayjs": "^1.11.11", + "dotenv": "^17.0.1", "humps": "^2.0.1" }, "devDependencies": { "@size-limit/preset-small-lib": "^7.0.8", "@swc/core": "^1.10.4", + "@types/async-retry": "^1.4.9", "@types/humps": "^2.0.1", + "@types/node": "^24.0.10", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", "@vitest/coverage-c8": "^0.24.4", - "cross-fetch": "^3.1.5", "esbuild-plugin-umd-wrapper": "^3.0.0", "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", @@ -78,4 +83,4 @@ "engines": { "node": ">=12" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9672e8..71eb967 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,18 @@ importers: .: dependencies: + async-retry: + specifier: ^1.3.3 + version: 1.3.3 + cross-fetch: + specifier: ^3.1.5 + version: 3.1.5 + dayjs: + specifier: ^1.11.11 + version: 1.11.13 + dotenv: + specifier: ^17.0.1 + version: 17.0.1 humps: specifier: ^2.0.1 version: 2.0.1 @@ -18,9 +30,15 @@ importers: '@swc/core': specifier: ^1.10.4 version: 1.10.4 + '@types/async-retry': + specifier: ^1.4.9 + version: 1.4.9 '@types/humps': specifier: ^2.0.1 version: 2.0.1 + '@types/node': + specifier: ^24.0.10 + version: 24.0.10 '@typescript-eslint/eslint-plugin': specifier: ^5.42.0 version: 5.42.0(@typescript-eslint/parser@5.42.0(eslint@8.26.0)(typescript@4.6.2))(eslint@8.26.0)(typescript@4.6.2) @@ -30,9 +48,6 @@ importers: '@vitest/coverage-c8': specifier: ^0.24.4 version: 0.24.4 - cross-fetch: - specifier: ^3.1.5 - version: 3.1.5 esbuild-plugin-umd-wrapper: specifier: ^3.0.0 version: 3.0.0 @@ -239,6 +254,9 @@ packages: '@swc/types@0.1.17': resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + '@types/async-retry@1.4.9': + resolution: {integrity: sha512-s1ciZQJzRh3708X/m3vPExr5KJlzlZJvXsKpbtE2luqNcbROr64qU+3KpJsYHqWMeaxI839OvXf9PrUSw1Xtyg==} + '@types/chai-subset@1.3.3': resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} @@ -260,8 +278,11 @@ packages: '@types/json-schema@7.0.10': resolution: {integrity: sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A==} - '@types/node@17.0.22': - resolution: {integrity: sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==} + '@types/node@24.0.10': + resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} + + '@types/retry@0.12.5': + resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} '@types/semver@7.3.13': resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} @@ -378,6 +399,9 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -496,6 +520,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -523,6 +550,10 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dotenv@17.0.1: + resolution: {integrity: sha512-GLjkduuAL7IMJg/ZnOPm9AnWKJ82mSE2tzXLaJ/6hD6DhwGfZaXG77oB8qbReyiczNxnbxQKyh0OE5mXq0bAHA==} + engines: {node: '>=12'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1370,6 +1401,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1593,6 +1628,9 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1855,6 +1893,10 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@types/async-retry@1.4.9': + dependencies: + '@types/retry': 0.12.5 + '@types/chai-subset@1.3.3': dependencies: '@types/chai': 4.3.3 @@ -1871,13 +1913,17 @@ snapshots: '@types/json-schema@7.0.10': {} - '@types/node@17.0.22': {} + '@types/node@24.0.10': + dependencies: + undici-types: 7.8.0 + + '@types/retry@0.12.5': {} '@types/semver@7.3.13': {} '@types/set-cookie-parser@2.4.2': dependencies: - '@types/node': 17.0.22 + '@types/node': 24.0.10 '@typescript-eslint/eslint-plugin@5.42.0(@typescript-eslint/parser@5.42.0(eslint@8.26.0)(typescript@4.6.2))(eslint@8.26.0)(typescript@4.6.2)': dependencies: @@ -2017,6 +2063,10 @@ snapshots: assertion-error@1.1.0: {} + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -2149,6 +2199,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + dayjs@1.11.13: {} + debug@4.3.4: dependencies: ms: 2.1.2 @@ -2171,6 +2223,8 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv@17.0.1: {} + emoji-regex@8.0.0: {} esbuild-android-64@0.14.27: @@ -2920,6 +2974,8 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry@0.13.1: {} + reusify@1.0.4: {} rimraf@3.0.2: @@ -3117,6 +3173,8 @@ snapshots: typescript@4.6.2: {} + undici-types@7.8.0: {} + uri-js@4.4.1: dependencies: punycode: 2.1.1 @@ -3142,7 +3200,7 @@ snapshots: dependencies: '@types/chai': 4.3.3 '@types/chai-subset': 1.3.3 - '@types/node': 17.0.22 + '@types/node': 24.0.10 chai: 4.3.6 debug: 4.3.4 local-pkg: 0.4.2 diff --git a/src/auth/tokenManager.ts b/src/auth/tokenManager.ts new file mode 100644 index 0000000..c613260 --- /dev/null +++ b/src/auth/tokenManager.ts @@ -0,0 +1,47 @@ +import retry from 'async-retry'; +import dayjs from 'dayjs'; +import { getConfig } from '../config'; + +let cachedToken: { value: string; expiresAt: number } | null = null; + +export async function getAccessToken() { + if (cachedToken && cachedToken.expiresAt > Date.now() + 30_000) { + return cachedToken.value; // still fresh + } + + const { clientId, clientSecret, authBaseUrl, fetchFn } = getConfig(); + const doFetch = fetchFn ?? globalThis.fetch; + + const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + + const body = new URLSearchParams({ + grant_type: 'client_credentials', + scope: 'content', + }).toString(); + + const res = await retry( + () => + doFetch(`${authBaseUrl}/oauth2/token`, { + method: 'POST', + headers: { + Authorization: `Basic ${auth}`, + 'content-type': 'application/x-www-form-urlencoded', + }, + body, + }), + { retries: 3 } + ); + + if (!res.ok) throw new Error(`Token request failed: ${res.statusText}`); + + const json = (await res.json()) as { + access_token: string; + expires_in: number; + }; + + cachedToken = { + value: json.access_token, + expiresAt: dayjs().add(json.expires_in, 'second').valueOf(), + }; + return cachedToken.value; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..09387fe --- /dev/null +++ b/src/config.ts @@ -0,0 +1,26 @@ +export interface SdkConfig { + clientId: string; + clientSecret: string; + contentBaseUrl?: string; // for /content/api/v4 + authBaseUrl?: string; // for /oauth2/token + + fetchFn?: typeof fetch; +} + +let sdkConfig: SdkConfig; + +export function configure(config: SdkConfig) { + sdkConfig = { + contentBaseUrl: 'https://apis.quran.foundation', // ✅ for all content endpoints + authBaseUrl: 'https://oauth2.quran.foundation', // ✅ for token endpoint + + ...config, + }; +} + +export function getConfig(): SdkConfig { + if (!sdkConfig) { + throw new Error('SDK not configured – call configure() first'); + } + return sdkConfig; +} diff --git a/src/index.ts b/src/index.ts index 409b7e0..1ebe5a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ +export { configure } from './config'; export { default as quran } from './sdk'; export * from './types'; diff --git a/src/sdk/index.ts b/src/sdk/index.ts index d420ee1..8238a2b 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -1,8 +1,8 @@ -import v4 from './v4'; +import qf from './qf'; import utils from './utils'; const quran = { - v4, + qf, utils, }; diff --git a/src/sdk/qf/_fetcher.ts b/src/sdk/qf/_fetcher.ts new file mode 100644 index 0000000..caa2941 --- /dev/null +++ b/src/sdk/qf/_fetcher.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import humps from 'humps'; +import { getAccessToken } from '../../auth/tokenManager'; +import { getConfig } from '../../config'; +import { removeBeginningSlash } from '../../utils/misc'; + +const { camelizeKeys, decamelizeKeys } = humps; + +export const makeUrl = (url: string, params: Record = {}) => { + const { contentBaseUrl } = getConfig(); + const apiRoot = `${contentBaseUrl}/content/api/v4/`; + const u = `${apiRoot}${removeBeginningSlash(url)}`; + + if (!Object.keys(params).length) return u; + + const qs = new URLSearchParams( + Object.entries(decamelizeKeys(params)).filter(([, v]) => v !== undefined) + ).toString(); + return qs ? `${u}?${qs}` : u; +}; + +export async function fetcher( + url: string, + params?: Record, + fetchFn?: typeof fetch +): Promise { + const token = await getAccessToken(); // auto-refresh + const { clientId } = getConfig(); + const doFetch = fetchFn ?? globalThis.fetch; + + const fullUrl = makeUrl(url, params); + + const res = await doFetch(fullUrl, { + headers: { + 'x-auth-token': token, + 'x-client-id': clientId, + }, + }); + + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + + return camelizeKeys(await res.json()) as T; +} diff --git a/src/sdk/qf/audio.ts b/src/sdk/qf/audio.ts new file mode 100644 index 0000000..0863965 --- /dev/null +++ b/src/sdk/qf/audio.ts @@ -0,0 +1,247 @@ +import { + ChapterRecitation, + VerseRecitation, + ChapterId, + HizbNumber, + JuzNumber, + Language, + PageNumber, + Pagination, + RubNumber, + VerseKey, + VerseRecitationField, +} from '../../types'; +import Utils from '../utils'; +import { fetcher } from './_fetcher'; +import { BaseApiOptions } from '../../types/BaseApiOptions'; +import { mergeApiOptions } from '../../utils/misc'; + +type GetChapterRecitationOptions = Partial; + +const defaultChapterRecitationsOptions: GetChapterRecitationOptions = { + language: Language.ARABIC, +}; + +type GetVerseRecitationOptions = Partial< + BaseApiOptions & { + fields: Partial>; + } +>; + +const defaultVerseRecitationsOptions: GetVerseRecitationOptions = { + language: Language.ARABIC, +}; + +/** + * Get all chapter recitations for specific reciter + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/chapter-reciter-audio-files + * @param {string} reciterId + * @param {GetChapterRecitationOptions} options + * @example + * quran.v4.audio.findAllChapterRecitations('2') + */ +const findAllChapterRecitations = async ( + reciterId: string, + options?: GetChapterRecitationOptions +) => { + const params = mergeApiOptions(defaultChapterRecitationsOptions, options); + const { audioFiles } = await fetcher<{ audioFiles: ChapterRecitation[] }>( + `/chapter_recitations/${reciterId}`, + params, + options?.fetchFn + ); + return audioFiles; +}; + +/** + * Get chapter recitation for specific reciter and a specific chapter + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/chapter-reciter-audio-file + * @param {ChapterId} chapterId + * @param {string} reciterId + * @param {GetChapterRecitationOptions} options + * @example + * quran.v4.audio.findChapterRecitationById('1', '2') // first chapter recitation for reciter 2 + */ +const findChapterRecitationById = async ( + chapterId: ChapterId, + reciterId: string, + options?: GetChapterRecitationOptions +) => { + if (!Utils.isValidChapterId(chapterId)) throw new Error('Invalid chapter id'); + + const params = mergeApiOptions(defaultChapterRecitationsOptions, options); + const { audioFile } = await fetcher<{ audioFile: ChapterRecitation }>( + `/chapter_recitations/${reciterId}/${chapterId}`, + params, + options?.fetchFn + ); + + return audioFile; +}; + +/** + * Get all verse audio files for a specific reciter and a specific chapter + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/list-surah-recitation + * @param {ChapterId} chapterId + * @param {string} recitationId + * @param {GetVerseRecitationOptions} options + * @example + * quran.v4.audio.findVerseRecitationsByChapter('1', '2') + */ +const findVerseRecitationsByChapter = async ( + chapterId: ChapterId, + recitationId: string, + options?: GetVerseRecitationOptions +) => { + if (!Utils.isValidChapterId(chapterId)) throw new Error('Invalid chapter id'); + + const params = mergeApiOptions(defaultVerseRecitationsOptions, options); + const data = await fetcher<{ + audioFiles: VerseRecitation[]; + pagination: Pagination; + }>( + `/recitations/${recitationId}/by_chapter/${chapterId}`, + params, + options?.fetchFn + ); + + return data; +}; + +/** + * Get all verse audio files for a specific reciter and a specific juz + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/list-juz-recitaiton + * @param {JuzNumber} juz + * @param {string} recitationId + * @param {GetRecitationsOptions} options + * @example + * quran.v4.audio.findVerseRecitationsByJuz('1', '2') + */ +const findVerseRecitationsByJuz = async ( + juz: JuzNumber, + recitationId: string, + options?: GetVerseRecitationOptions +) => { + if (!Utils.isValidJuz(juz)) throw new Error('Invalid juz'); + + const params = mergeApiOptions(defaultVerseRecitationsOptions, options); + const data = await fetcher<{ + audioFiles: VerseRecitation[]; + pagination: Pagination; + }>(`/recitations/${recitationId}/by_juz/${juz}`, params, options?.fetchFn); + + return data; +}; + +/** + * Get all verse audio files for a specific reciter and a specific mushaf page + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/list-page-recitaiton + * @param {PageNumber} page + * @param {string} recitationId + * @param {GetVerseRecitationOptions} options + * @example + * quran.v4.audio.findVerseRecitationsByPage('1', '2') + */ +const findVerseRecitationsByPage = async ( + page: PageNumber, + recitationId: string, + options?: GetVerseRecitationOptions +) => { + if (!Utils.isValidQuranPage(page)) throw new Error('Invalid page'); + + const params = mergeApiOptions(defaultVerseRecitationsOptions, options); + const data = await fetcher<{ + audioFiles: VerseRecitation[]; + pagination: Pagination; + }>(`/recitations/${recitationId}/by_page/${page}`, params, options?.fetchFn); + + return data; +}; + +/** + * Get all verse audio files for a specific reciter and a specific rub + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/list-rub-el-hizb-recitaiton + * @param {RubNumber} rub + * @param {string} recitationId + * @param {GetVerseRecitationOptions} options + * @example + * quran.v4.audio.findVerseRecitationsByRub('1', '2') + */ +const findVerseRecitationsByRub = async ( + rub: RubNumber, + recitationId: string, + options?: GetVerseRecitationOptions +) => { + if (!Utils.isValidRub(rub)) throw new Error('Invalid rub'); + + const params = mergeApiOptions(defaultVerseRecitationsOptions, options); + const data = await fetcher<{ + audioFiles: VerseRecitation[]; + pagination: Pagination; + }>(`/recitations/${recitationId}/by_rub/${rub}`, params, options?.fetchFn); + + return data; +}; + +/** + * Get all verse audio files for a specific reciter and a specific hizb + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/list-hizb-recitaiton + * @param {HizbNumber} hizb + * @param {string} recitationId + * @param {GetVerseRecitationOptions} options + * @example + * quran.v4.audio.findVerseRecitationsByHizb('1', '2') + */ +const findVerseRecitationsByHizb = async ( + hizb: HizbNumber, + recitationId: string, + options?: GetVerseRecitationOptions +) => { + if (!Utils.isValidHizb(hizb)) throw new Error('Invalid hizb'); + + const params = mergeApiOptions(defaultVerseRecitationsOptions, options); + const data = await fetcher<{ + audioFiles: VerseRecitation[]; + pagination: Pagination; + }>(`/recitations/${recitationId}/by_hizb/${hizb}`, params, options?.fetchFn); + + return data; +}; + +/** + * Get all verse audio files for a specific reciter and a specific verse + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/list-ayah-recitaiton + * @param {VerseKey} key + * @param {string} recitationId + * @param {GetVerseRecitationOptions} options + * @example + * quran.v4.audio.findVerseRecitationsByKey('1:1', '2') + */ +const findVerseRecitationsByKey = async ( + key: VerseKey, + recitationId: string, + options?: GetVerseRecitationOptions +) => { + if (!Utils.isValidVerseKey(key)) throw new Error('Invalid verse key'); + + const params = mergeApiOptions(defaultVerseRecitationsOptions, options); + const data = await fetcher<{ + audioFiles: VerseRecitation[]; + pagination: Pagination; + }>(`/recitations/${recitationId}/by_ayah/${key}`, params, options?.fetchFn); + + return data; +}; + +const audio = { + findAllChapterRecitations, + findChapterRecitationById, + findVerseRecitationsByChapter, + findVerseRecitationsByJuz, + findVerseRecitationsByPage, + findVerseRecitationsByRub, + findVerseRecitationsByHizb, + findVerseRecitationsByKey, +}; + +export default audio; diff --git a/src/sdk/qf/chapters.ts b/src/sdk/qf/chapters.ts new file mode 100644 index 0000000..878e5c6 --- /dev/null +++ b/src/sdk/qf/chapters.ts @@ -0,0 +1,81 @@ +import { Chapter, ChapterId, ChapterInfo, Language } from '../../types'; +import { fetcher } from './_fetcher'; +import Utils from '../utils'; +import { BaseApiOptions } from '../../types/BaseApiOptions'; +import { mergeApiOptions } from '../../utils/misc'; + +type GetChapterOptions = Partial; + +const defaultOptions: GetChapterOptions = { + language: Language.ARABIC, +}; + +/** + * Get all chapters. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/list-chapters + * @param {GetChapterOptions} options + * @example + * quran.v4.chapters.findAll() + */ +const findAll = async (options?: GetChapterOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { chapters } = await fetcher<{ chapters: Chapter[] }>( + '/chapters', + params, + options?.fetchFn + ); + + return chapters; +}; + +/** + * Get chapter by id. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/get-chapter + * @param {ChapterId} id chapter id, minimum 1, maximum 114 + * @param {GetChapterOptions} options + * @example + * quran.v4.chapters.findById('1') + * quran.v4.chapters.findById('114') + */ +const findById = async (id: ChapterId, options?: GetChapterOptions) => { + if (!Utils.isValidChapterId(id)) throw new Error('Invalid chapter id'); + + const params = mergeApiOptions(defaultOptions, options); + const { chapter } = await fetcher<{ chapter: Chapter }>( + `/chapters/${id}`, + params, + options?.fetchFn + ); + + return chapter; +}; + +/** + * Get chapter info by id. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/info + * @param {ChapterId} id chapter id, minimum 1, maximum 114 + * @param {GetChapterOptions} options + * @example + * quran.v4.chapters.findInfoById('1') + * quran.v4.chapters.findInfoById('114') + */ +const findInfoById = async (id: ChapterId, options?: GetChapterOptions) => { + if (!Utils.isValidChapterId(id)) throw new Error('Invalid chapter id'); + + const params = mergeApiOptions(defaultOptions, options); + const { chapterInfo } = await fetcher<{ chapterInfo: ChapterInfo }>( + `/chapters/${id}/info`, + params, + options?.fetchFn + ); + + return chapterInfo; +}; + +const chapters = { + findAll, + findById, + findInfoById, +}; + +export default chapters; diff --git a/src/sdk/qf/index.ts b/src/sdk/qf/index.ts new file mode 100644 index 0000000..5734506 --- /dev/null +++ b/src/sdk/qf/index.ts @@ -0,0 +1,17 @@ +import chapters from './chapters'; +import verses from './verses'; +import juzs from './juzs'; +import audio from './audio'; +import resources from './resources'; +import search from './search'; + +const qf = { + chapters, + verses, + juzs, + audio, + resources, + search, +}; + +export default qf; diff --git a/src/sdk/qf/juzs.ts b/src/sdk/qf/juzs.ts new file mode 100644 index 0000000..d8a78fd --- /dev/null +++ b/src/sdk/qf/juzs.ts @@ -0,0 +1,22 @@ +import { Juz } from '../../types'; +import { BaseApiOptions } from '../../types/BaseApiOptions'; +import { fetcher } from './_fetcher'; + +/** + * Get All Juzs + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/juzs + * @example + * quran.v4.juzs.findAll() + */ +const findAll = async (options?: Omit) => { + const { juzs } = await fetcher<{ juzs: Juz[] }>( + '/juzs', + undefined, + options?.fetchFn + ); + return juzs; +}; + +const juzs = { findAll }; + +export default juzs; diff --git a/src/sdk/qf/resources.ts b/src/sdk/qf/resources.ts new file mode 100644 index 0000000..115e97f --- /dev/null +++ b/src/sdk/qf/resources.ts @@ -0,0 +1,221 @@ +import { + ChapterInfoResource, + Language, + LanguageResource, + RecitationInfoResource, + RecitationResource, + RecitationStylesResource, + Reciter, + TafsirInfoResource, + TafsirResource, + TranslationInfoResource, + TranslationResource, + VerseMediaResource, +} from '../../types'; +import { BaseApiOptions } from '../../types/BaseApiOptions'; +import { mergeApiOptions } from '../../utils/misc'; +import { fetcher } from './_fetcher'; + +type GetResourceOptions = Partial; + +const defaultOptions: GetResourceOptions = { + language: Language.ARABIC, +}; + +/** + * Get all recitations. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/recitations + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findAllRecitations() + */ +const findAllRecitations = async (options?: GetResourceOptions) => { + const params = mergeApiOptions(options); + const { recitations } = await fetcher<{ + recitations: RecitationResource[]; + }>('/resources/recitations', params, options?.fetchFn); + + return recitations; +}; + +/** + * Get all recitation info. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/recitation-info + * @param {string} id recitation id + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findRecitationInfo('1') + */ +const findRecitationInfo = async (id: string, options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { info } = await fetcher<{ + info: RecitationInfoResource; + }>(`/resources/recitations/${id}/info`, params, options?.fetchFn); + + return info; +}; + +/** + * Get all translations. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/translations + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findAllTranslations() + */ +const findAllTranslations = async (options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { translations } = await fetcher<{ + translations: TranslationResource[]; + }>('/resources/translations', params, options?.fetchFn); + + return translations; +}; + +/** + * Get translation info. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/translation-info + * @param {string} id translation id + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findTranslationInfo('169') + */ +const findTranslationInfo = async ( + id: string, + options?: GetResourceOptions +) => { + const params = mergeApiOptions(defaultOptions, options); + const { info } = await fetcher<{ + info: TranslationInfoResource; + }>(`/resources/translations/${id}/info`, params, options?.fetchFn); + + return info; +}; + +/** + * Get all tafsirs. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/tafsirs + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findAllTafsirs() + */ +const findAllTafsirs = async (options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { tafsirs } = await fetcher<{ + tafsirs: TafsirResource[]; + }>('/resources/tafsirs', params, options?.fetchFn); + + return tafsirs; +}; + +/** + * Get tafsir info. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/tafsir-info + * @param {string} id tafsir id + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findTafsirInfo('1') + */ +const findTafsirInfo = async (id: string, options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { info } = await fetcher<{ + info: TafsirInfoResource; + }>(`/resources/tafsirs/${id}/info`, params, options?.fetchFn); + + return info; +}; + +/** + * Get all recitation styles. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/recitation-styles + * @example + * quran.v4.resources.findAllRecitationStyles() + */ +const findAllRecitationStyles = async ( + options?: Omit +) => { + const { recitationStyles } = await fetcher<{ + recitationStyles: RecitationStylesResource; + }>('/resources/recitation_styles', undefined, options?.fetchFn); + + return recitationStyles; +}; + +/** + * Get all languages. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/languages + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findAllLanguages() + */ +const findAllLanguages = async (options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { languages } = await fetcher<{ + languages: LanguageResource[]; + }>('/resources/languages', params, options?.fetchFn); + + return languages; +}; + +/** + * Get all chapter infos. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/chapter-info + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findAllChapterInfos() + */ +const findAllChapterInfos = async (options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { chapterInfos } = await fetcher<{ + chapterInfos: ChapterInfoResource[]; + }>('/resources/chapter_infos', params, options?.fetchFn); + + return chapterInfos; +}; + +/** + * Get verse media. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/verse-media + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findVerseMedia() + */ +const findVerseMedia = async (options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { verseMedia } = await fetcher<{ + verseMedia: VerseMediaResource; + }>(`/resources/verse_media`, params, options?.fetchFn); + + return verseMedia; +}; + +/** + * Get all chapter reciters. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/chapter-reciters + * @param {GetResourceOptions} options + * @example + * quran.v4.resources.findAllChapterReciters() + */ +const findAllChapterReciters = async (options?: GetResourceOptions) => { + const params = mergeApiOptions(defaultOptions, options); + const { reciters } = await fetcher<{ + reciters: Reciter[]; + }>(`/resources/chapter_reciters`, params, options?.fetchFn); + + return reciters; +}; + +const resources = { + findAllRecitations, + findAllTranslations, + findAllTafsirs, + findAllRecitationStyles, + findAllLanguages, + findVerseMedia, + findAllChapterReciters, + findAllChapterInfos, + findRecitationInfo, + findTranslationInfo, + findTafsirInfo, +}; + +export default resources; diff --git a/src/sdk/qf/search.ts b/src/sdk/qf/search.ts new file mode 100644 index 0000000..5fe277a --- /dev/null +++ b/src/sdk/qf/search.ts @@ -0,0 +1,44 @@ +import { SearchResponse } from '../../types'; +import { BaseApiOptions } from '../../types/BaseApiOptions'; +import { mergeApiOptions } from '../../utils/misc'; +import { fetcher } from './_fetcher'; + +type SearchOptions = Partial< + BaseApiOptions & { + q: string; + size: number; + page: number; + } +>; + +const defaultSearchOptions: SearchOptions = { + size: 30, +}; + +/** + * Search + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/search + * @param {string} q search query + * @param {SearchOptions} options + * @example + * quran.v4.search.search('نور') + * quran.v4.search.search('نور', { language: Language.ENGLISH }) + * quran.v4.search.search('نور', { language: Language.ENGLISH, size: 10 }) + * quran.v4.search.search('نور', { language: Language.ENGLISH, page: 2 }) + */ +const search = async (q: string, options?: SearchOptions) => { + + const params = mergeApiOptions(defaultSearchOptions, { q, ...options }); + + const { search } = await fetcher( + '/search', + params, + options?.fetchFn + ); + + return search; +}; + +const searchApi = { search }; + +export default searchApi; diff --git a/src/sdk/qf/verses.ts b/src/sdk/qf/verses.ts new file mode 100644 index 0000000..336743c --- /dev/null +++ b/src/sdk/qf/verses.ts @@ -0,0 +1,240 @@ +import { + ChapterId, + HizbNumber, + JuzNumber, + Language, + PageNumber, + RubNumber, + TranslationField, + Verse, + VerseField, + VerseKey, + WordField, +} from '../../types'; +import humps from 'humps'; +import Utils from '../utils'; +import { fetcher } from './_fetcher'; +import { BaseApiOptions } from '../../types/BaseApiOptions'; +import { mergeApiOptions } from '../../utils/misc'; + +const { decamelize } = humps; + +export type GetVerseOptions = Partial< + BaseApiOptions & { + reciter: string | number; + audio?: string | number; // ✅ required for internal rename + words: boolean; + translations: string[] | number[]; + tafsirs: string[] | number[] | string; // ✅ allow string too + wordFields: Partial> | string; // ✅ for .join + translationFields: Partial> | string; // ✅ for .join + fields: Partial> | string; // ✅ also may need string + page: number; + perPage: number; + } +>; + +const defaultOptions: GetVerseOptions = { + language: Language.ARABIC, + perPage: 50, + words: false, +}; + +const mergeVerseOptions = (options: GetVerseOptions = {}) => { + const result = mergeApiOptions(defaultOptions, options); + + // @ts-expect-error - we accept an array of strings, however, the API expects a comma separated string + if (result.translations) result.translations = result.translations.join(','); + + // @ts-expect-error - we accept an array of strings, however, the API expects a comma separated string + if (result.tafsirs) result.tafsirs = result.tafsirs.join(','); + + if (result.wordFields) { + const wordFields: string[] = []; + Object.entries(result.wordFields).forEach(([key, value]) => { + if (value) wordFields.push(decamelize(key)); + }); + result.wordFields = wordFields.join(','); + } + + if (result.translationFields) { + const translationFields: string[] = []; + Object.entries(result.translationFields).forEach(([key, value]) => { + if (value) translationFields.push(decamelize(key)); + }); + result.translationFields = translationFields.join(','); + } + + // rename `reciter` to `audio` because the API expects `audio` + if (result.reciter) { + result.audio = result.reciter; + result.reciter = undefined; + } + + return result; +}; + +/** + * Get a specific ayah with key. Key is combination of surah number and ayah number. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/verses-by-verse-key + * @param {VerseKey} key - surah number and ayah number separated by a colon. + * @param {GetVerseOptions} options + * @example + * quran.v4.verses.findByKey('1:1') + * quran.v4.verses.findByKey('101:5') + */ +const findByKey = async (key: VerseKey, options?: GetVerseOptions) => { + if (!Utils.isValidVerseKey(key)) throw new Error('Invalid verse key'); + const params = mergeVerseOptions(options); + const url = `/verses/by_key/${key}`; + const { verse } = await fetcher<{ verse: Verse }>( + url, + params, + options?.fetchFn + ); + + return verse; +}; + +/** + * Get all ayahs for a specific chapter (surah). + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/verses-by-chapter-number + * @param {ChapterId} id - chapter id (surah number) + * @param {GetVerseOptions} options + * @example + * quran.v4.verses.findByChapter('1') + * quran.v4.verses.findByChapter('101') + */ +const findByChapter = async (id: ChapterId, options?: GetVerseOptions) => { + if (!Utils.isValidChapterId(id)) throw new Error('Invalid chapter id'); + const params = mergeVerseOptions(options); + const url = `/verses/by_chapter/${id}`; + const { verses } = await fetcher<{ verses: Verse[] }>( + url, + params, + options?.fetchFn + ); + + return verses; +}; + +/** + * Get all ayahs for a specific page in the Quran. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/verses-by-page-number + * @param {PageNumber} page - Quran page number + * @param {GetVerseOptions} options + * @example + * quran.v4.verses.findByPage('1') + * quran.v4.verses.findByPage('101') + */ +const findByPage = async (page: PageNumber, options?: GetVerseOptions) => { + if (!Utils.isValidQuranPage(page)) throw new Error('Invalid page'); + + const params = mergeVerseOptions(options); + const url = `/verses/by_page/${page}`; + const { verses } = await fetcher<{ verses: Verse[] }>( + url, + params, + options?.fetchFn + ); + + return verses; +}; + +/** + * Get all ayahs for a Juz. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/verses-by-juz-number + * @param {JuzNumber} juz - juz number + * @param {GetVerseOptions} options + * @example + * quran.v4.verses.findByJuz('1') + * quran.v4.verses.findByJuz('29') + */ +const findByJuz = async (juz: JuzNumber, options?: GetVerseOptions) => { + if (!Utils.isValidJuz(juz)) throw new Error('Invalid juz'); + + const params = mergeVerseOptions(options); + const url = `/verses/by_juz/${juz}`; + const { verses } = await fetcher<{ verses: Verse[] }>( + url, + params, + options?.fetchFn + ); + + return verses; +}; + +/** + * Get all ayahs for a Hizb. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/verses-by-hizb-number + * @param {HizbNumber} hizb - hizb number + * @param {GetVerseOptions} options + * @example + * quran.v4.verses.findByHizb('1') + * quran.v4.verses.findByHizb('29') + */ +const findByHizb = async (hizb: HizbNumber, options?: GetVerseOptions) => { + if (!Utils.isValidHizb(hizb)) throw new Error('Invalid hizb'); + + const params = mergeVerseOptions(options); + const url = `/verses/by_hizb/${hizb}`; + const { verses } = await fetcher<{ verses: Verse[] }>( + url, + params, + options?.fetchFn + ); + + return verses; +}; + +/** + * Get all ayahs for a Rub. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/verses-by-rub-el-hizb-number + * @param {RubNumber} rub - rub number + * @param {GetVerseOptions} options + * @example + * quran.v4.verses.findByRub('1') + * quran.v4.verses.findByRub('29') + */ +const findByRub = async (rub: RubNumber, options?: GetVerseOptions) => { + if (!Utils.isValidRub(rub)) throw new Error('Invalid rub'); + + const params = mergeVerseOptions(options); + const { verses } = await fetcher<{ verses: Verse[] }>( + `/verses/by_rub/${rub}`, + params, + options?.fetchFn + ); + + return verses; +}; + +/** + * Get a random ayah. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/random-verse + * @param {GetVerseOptions} options + * @example + * quran.v4.verses.findRandom() + */ +const findRandom = async (options?: GetVerseOptions) => { + const params = mergeVerseOptions(options); + const { verse } = await fetcher<{ verse: Verse }>( + '/verses/random', + params, + options?.fetchFn + ); + + return verse; +}; + +const verses = { + findByKey, + findByChapter, + findByPage, + findByJuz, + findByHizb, + findByRub, + findRandom, +}; + +export default verses; diff --git a/src/types/BaseApiOptions.ts b/src/types/BaseApiOptions.ts index 0d7ff7a..ffa14fd 100644 --- a/src/types/BaseApiOptions.ts +++ b/src/types/BaseApiOptions.ts @@ -1,6 +1,6 @@ -import { FetchFn, Language } from '.'; +import { Language } from '.'; export interface BaseApiOptions { language: Language; - fetchFn?: FetchFn; + fetchFn?: typeof fetch; } diff --git a/src/types/api/ApiResponses.ts b/src/types/api/ApiResponses.ts index 3082cb9..383907a 100644 --- a/src/types/api/ApiResponses.ts +++ b/src/types/api/ApiResponses.ts @@ -1,19 +1,11 @@ -import { Translation } from './Translation'; -import { Word } from './Word'; +import { SearchResult } from "./searchResult"; export interface SearchResponse { search: { query: string; - totalResults: number; - currentPage: number; - totalPages: number; - results?: { - verseKey: string; - verse_id: number; - text: string; - highlighted: string; - words: Word[]; - translations: Translation[]; - }[]; + total_results: number; + current_page: number; + total_pages: number; + results?: SearchResult[]; }; } diff --git a/src/types/api/AudioData.ts b/src/types/api/AudioData.ts index d195773..d5d6cc9 100644 --- a/src/types/api/AudioData.ts +++ b/src/types/api/AudioData.ts @@ -3,18 +3,17 @@ import { Segment } from './Segment'; export interface ChapterRecitation { id: number; - chapterId: number; - fileSize: number; + chapter_id: number; + file_size: number; format: string; - audioUrl: string; + audio_url: string; } export interface VerseRecitation { verseKey: VerseKey; url: string; - id?: number; - chapterId?: number; + chapter_id?: number; segments?: Segment[]; format?: string; } diff --git a/src/types/api/AudioResponse.ts b/src/types/api/AudioResponse.ts index 5e46d3f..cfe5ddd 100644 --- a/src/types/api/AudioResponse.ts +++ b/src/types/api/AudioResponse.ts @@ -1,9 +1,9 @@ import { Segment } from './Segment'; export interface AudioResponse { - url?: string; + url: string; duration?: number; format?: string; verseKey: string; - segments?: Segment[]; + segments: Segment[]; } diff --git a/src/types/api/Chapter.ts b/src/types/api/Chapter.ts index 5bfe2e9..08106e9 100644 --- a/src/types/api/Chapter.ts +++ b/src/types/api/Chapter.ts @@ -2,14 +2,13 @@ import { TranslatedName } from './TranslatedName'; export interface Chapter { id: number; - versesCount: number; - bismillahPre: boolean; - revelationOrder: number; - revelationPlace: string; - pages: Array; - nameComplex: string; - nameSimple: string; - transliteratedName: string; - nameArabic: string; - translatedName: TranslatedName; + revelation_place: string; + revelation_order: number; + bismillah_pre: boolean; + name_simple: string; + name_complex: string; + name_arabic: string; + verses_count: number; + pages: number[]; + translated_name: TranslatedName; } diff --git a/src/types/api/ChapterInfo.ts b/src/types/api/ChapterInfo.ts index 88ae9b0..6f57f76 100644 --- a/src/types/api/ChapterInfo.ts +++ b/src/types/api/ChapterInfo.ts @@ -1,8 +1,8 @@ export interface ChapterInfo { id: number; - chapterId: number; + chapter_id: number; text: string; - shortText: string; + short_text: string; source: string; - languageName?: string; + language_name?: string; } diff --git a/src/types/api/Footnote.ts b/src/types/api/Footnote.ts index 7185e8b..e26e9fe 100644 --- a/src/types/api/Footnote.ts +++ b/src/types/api/Footnote.ts @@ -1,6 +1,6 @@ export interface Footnote { id: number | string; text: string; - languageName?: string; - languageId?: number; + language_name?: string; + language_id?: number; } diff --git a/src/types/api/Juz.ts b/src/types/api/Juz.ts index 37ef0d9..3036fce 100644 --- a/src/types/api/Juz.ts +++ b/src/types/api/Juz.ts @@ -1,8 +1,8 @@ export interface Juz { id: number; - juzNumber: number; - verseMapping: Record; - firstVerseId: number; - lastVerseId: number; - versesCount: number; + juz_number: number; + verse_mapping: Record; + first_verse_id: number; + last_verse_id: number; + verses_count: number; } diff --git a/src/types/api/Pagination.ts b/src/types/api/Pagination.ts index 934a653..7e203dd 100644 --- a/src/types/api/Pagination.ts +++ b/src/types/api/Pagination.ts @@ -1,7 +1,7 @@ export interface Pagination { - perPage: number; - currentPage: number; - nextPage: number; - totalPages: number; - totalRecords: number; + per_page: number; + current_page: number; + next_page: number | null; + total_pages: number; + total_records: number; } diff --git a/src/types/api/Reciter.ts b/src/types/api/Reciter.ts index c2d4b4c..523a2ff 100644 --- a/src/types/api/Reciter.ts +++ b/src/types/api/Reciter.ts @@ -1,21 +1,18 @@ +import { TranslatedName } from "./TranslatedName"; + export interface Reciter { id: number; name: string; - recitationStyle: string; - relativePath: string; - profilePicture?: string; - coverImage?: string; + profile_picture?: string; + cover_image?: string; bio?: string; - qirat?: { - languageName: string; + qirat: { + language_name: string; name: string; }; - style?: { - languageName: string; - name: string; - }; - translatedName?: { - languageName: string; + style: { + language_name: string; name: string; }; + translated_name: TranslatedName } diff --git a/src/types/api/Resources.ts b/src/types/api/Resources.ts index 5b3155e..6561b77 100644 --- a/src/types/api/Resources.ts +++ b/src/types/api/Resources.ts @@ -1,81 +1,75 @@ import { TranslatedName } from './TranslatedName'; export interface RecitationResource { - id?: number; - reciterName?: string; - style?: string; - translatedName?: TranslatedName; + id: number; + reciter_name: string; + style: string; + translated_name?: TranslatedName; } export interface RecitationInfoResource { - id?: number; - info?: string; + id: number; + info: string; } export interface TranslationResource { - id?: number; - name?: string; - authorName?: string; - slug?: string; - languageName?: string; - translatedName?: TranslatedName; + id: number; + name: string; + author_name: string; + slug: string; + language_name: string; + translated_name: TranslatedName; } export interface TranslationInfoResource { - id?: number; - info?: string; + id: number; + info: string; } export interface TafsirResource { - id?: number; - name?: string; - authorName?: string; - slug?: string; - languageName?: string; - translatedName?: TranslatedName; + id: number; + name: string; + author_name: string; + slug: string; + language_name: string; + translated_name: TranslatedName; } export interface TafsirInfoResource { - id?: number; - info?: string; + id: number; + info: string; } export interface RecitationStylesResource { - mujawwad: string; - murattal: string; - muallim: string; + mujawwad: 'Mujawwad is a melodic style of Holy Quran recitation'; + murattal: 'Murattal is at a slower pace, used for study and practice'; + muallim: 'Muallim is teaching style recitation of Holy Quran'; } export interface LanguageResource { - id?: number; - name?: string; - nativeName?: string; - isoCode?: string; - direction?: string; - translatedNames?: TranslatedName[]; + id: number; + name: string; + native_name: string; + iso_code: string; + direction: string; + translations_count: number; + translated_names: TranslatedName[]; } export interface ChapterInfoResource { - id?: number; - name?: string; - authorName?: string; - slug?: string; - languageName?: string; - translatedName?: TranslatedName; + id: number; + name: string; + author_name: string; + slug: string; + language_name: string; + translated_name: TranslatedName; } export interface VerseMediaResource { - id?: number; - name?: string; - authorName?: string; - languageName?: string; -} - -export interface ChapterReciterResource { id: number; name: string; - arabicName?: string; - relativePath?: string; - format?: string; - filesSize?: number; // in kb -} + author_name: string; + slug: string; + language_name: string; + translated_name: TranslatedName;} + diff --git a/src/types/api/Tafsir.ts b/src/types/api/Tafsir.ts index 83f7aaa..997638f 100644 --- a/src/types/api/Tafsir.ts +++ b/src/types/api/Tafsir.ts @@ -1,7 +1,7 @@ export interface Tafsir { - id?: number; - resourceId?: number; - text?: string; - resourceName?: string; - languageName?: string; + id: number; + resource_id?: number; + text: string; + resource_name?: string; + language_name?: string; } diff --git a/src/types/api/TafsirInfo.ts b/src/types/api/TafsirInfo.ts index 311af96..461f075 100644 --- a/src/types/api/TafsirInfo.ts +++ b/src/types/api/TafsirInfo.ts @@ -3,8 +3,8 @@ import { TranslatedName } from './TranslatedName'; export interface TafsirInfo { id?: number; name?: string; - authorName?: string; + author_name?: string; slug?: string; - languageName?: string; - translatedName: TranslatedName; + language_name?: string; + translated_name: TranslatedName; } diff --git a/src/types/api/TranslatedName.ts b/src/types/api/TranslatedName.ts index 6fde739..42332e4 100644 --- a/src/types/api/TranslatedName.ts +++ b/src/types/api/TranslatedName.ts @@ -1,4 +1,4 @@ export interface TranslatedName { name: string; - languageName: string; + language_name: string; } diff --git a/src/types/api/Translation.ts b/src/types/api/Translation.ts index cd4f7e2..e5f0885 100644 --- a/src/types/api/Translation.ts +++ b/src/types/api/Translation.ts @@ -1,21 +1,21 @@ import { VerseKey } from '../VerseKey'; export interface Translation { - id?: number; + id: number; text: string; - resourceId: number; - resourceName?: string; + resource_id: number; + resource_name?: string; - verseId?: number; + verse_id?: number; - languageId?: number; - languageName?: string; + language_id?: number; + language_name?: string; - verseKey?: VerseKey; - chapterId?: number; - verseNumber?: number; - juzNumber?: number; - hizbNumber?: number; - rubNumber?: number; - pageNumber?: number; + verse_key?: VerseKey; + chapter_id?: number; + verse_number?: number; + juz_number?: number; + hizb_number?: number; + rub_number?: number; + page_number?: number; } diff --git a/src/types/api/Transliteration.ts b/src/types/api/Transliteration.ts index 5af7313..a9877c4 100644 --- a/src/types/api/Transliteration.ts +++ b/src/types/api/Transliteration.ts @@ -1,4 +1,4 @@ export interface Transliteration { - languageName?: string; + language_name?: string; text?: string; } diff --git a/src/types/api/Verse.ts b/src/types/api/Verse.ts index 8b67e1a..4895ffe 100644 --- a/src/types/api/Verse.ts +++ b/src/types/api/Verse.ts @@ -6,31 +6,36 @@ import { Word } from './Word'; export interface Verse { id: number; - verseNumber: number; - verseKey: VerseKey; - chapterId?: number | string; - pageNumber: number; - juzNumber: number; - hizbNumber: number; - rubElHizbNumber: number; - // verseIndex: number; + verse_key: VerseKey; + verse_number: number; + chapter_id?: number | string; + page_number: number; + juz_number: number; + hizb_number: number; + rub_el_hizb_number: number; words?: Word[]; - textUthmani?: string; - textUthmaniSimple?: string; - textUthmaniTajweed?: string; - textImlaei?: string; - textImlaeiSimple?: string; - textIndopak?: string; - textIndopakNastaleeq?: string; - sajdahNumber: null; - // sajdahType: null; - imageUrl?: string; - imageWidth?: number; - v1Page?: number; - v2Page?: number; - codeV1?: string; - codeV2?: string; + + text_uthmani?: string; + text_uthmani_simple?: string; + text_uthmani_tajweed?: string; + text_imlaei?: string; + text_imlaei_simple?: string; + text_indopak?: string; + text_indopak_nastaleeq?: string; + + sajdah_number?: number | null; + sajdah_type?: string | null; + + image_url?: string; + image_width?: number; + + v1_page?: number; + v2_page?: number; + + code_v1?: string; + code_v2?: string; + translations?: Translation[]; tafsirs?: Tafsir[]; audio?: AudioResponse; -} +} \ No newline at end of file diff --git a/src/types/api/Word.ts b/src/types/api/Word.ts index 9258f21..adb4800 100644 --- a/src/types/api/Word.ts +++ b/src/types/api/Word.ts @@ -13,18 +13,19 @@ export enum CharType { export interface Word { id?: number; position: number; - audioUrl: string; - charTypeName: CharType; - codeV1?: string; - codeV2?: string; - pageNumber?: number; - lineNumber?: number; - text?: string; - textUthmani?: string; - textIndopak?: string; - textImlaei?: string; - translation: Translation; - transliteration: Transliteration; - location?: string; // chapter:verse:word - verseKey?: VerseKey; + audio_url: string; + char_type: CharType + text_uthmani?: string; + text_indopak?: string; + text_imlaei?: string; + verse_key?: VerseKey + page_number?: number; + line_number?: number; + code_v1?: string; + code_v2?: string; + translation: Translation + transliteration: Transliteration + location?: string; + v1_page?: number; + v2_page?: number; } diff --git a/src/types/api/searchResult.ts b/src/types/api/searchResult.ts new file mode 100644 index 0000000..dd29f0b --- /dev/null +++ b/src/types/api/searchResult.ts @@ -0,0 +1,12 @@ +import { VerseKey } from "../VerseKey"; +import { Translation } from "./Translation"; +import { Word } from "./Word"; + +export interface SearchResult { + verse_key: VerseKey + verse_id: number; + text: string; + highlighted?: string; + words: Word[]; + translations: Translation[]; +} diff --git a/src/utils/misc.ts b/src/utils/misc.ts index f68039d..4352bf8 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -1,3 +1,7 @@ export const removeBeginningSlash = (url: string) => { return url.startsWith('/') ? url.slice(1) : url; }; + +export function mergeApiOptions(defaults: T, overrides?: Partial): T { + return Object.assign({}, defaults, overrides); +} diff --git a/test/manual.test.ts b/test/manual.test.ts new file mode 100644 index 0000000..e0ef161 --- /dev/null +++ b/test/manual.test.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { configure, quran } from '../src'; + +(async () => { + try { + // 1. Configure the SDK + configure({ + clientId: process.env.QF_CLIENT_ID!, + clientSecret: process.env.QF_CLIENT_SECRET!, + }); + + // 2. Chapters + const chapters = await quran.qf.chapters.findAll(); + console.log(`✅ Chapters: ${chapters.length}`); + + const chapter = await quran.qf.chapters.findById(1); + console.log(`✅ Chapter 1 name: ${chapter.name_arabic}`); + + // 3. Verses + const verses = await quran.qf.verses.findByChapter(1, { + page: 1, + perPage: 5, + }); + console.log(`✅ Verses from chapter 1: ${verses.length}`); + + const verseById = await quran.qf.verses.findByKey('1:1'); + console.log(`✅ Verse 1 text: ${verseById.text_uthmani}}`); + + const juzVerses = await quran.qf.verses.findByJuz(1, { + perPage: 5, + }); + console.log(`✅ Verses from juz 1: ${juzVerses.length}`); + + // 4. Juzs + const juzs = await quran.qf.juzs.findAll(); + console.log(`✅ Juzs: ${juzs.length}`); + + const juz = await quran.qf.juzs.findAll(); + console.log(`✅ Juz 1: ${juz.length} verse mappings`); + + // 5. Resources + const resources = await quran.qf.resources.findAllChapterInfos(); + console.log(`✅ Resources: ${resources.length}`); + + const translations = await quran.qf.resources.findAllTranslations(); + console.log(`✅ Translations: ${translations.length}`); + + const tafsirs = await quran.qf.resources.findAllTafsirs(); + console.log(`✅ Tafsirs: ${tafsirs.length}`); + + const reciters = await quran.qf.resources.findAllRecitations(); + console.log(`✅ Reciters: ${reciters.length}`); + + // 6. Audio (chapter-reciter combo) + const audioFiles = await quran.qf.audio.findAllChapterRecitations('1'); + console.log(`✅ Audio files for Chapter 1: ${audioFiles.length}`); + + // 7. Search + const search = await quran.qf.search.search('نور'); + console.log(`✅ Search for "نور": ${search.results?.length} matches`); + + console.log('🎉 ALL TESTS PASSED SUCCESSFULLY 🎉'); + } catch (err) { + console.error('❌ TEST FAILED:', err); + } +})();