diff --git a/apps/docs/content/docs/tafsir.mdx b/apps/docs/content/docs/tafsir.mdx new file mode 100644 index 0000000..4abbf44 --- /dev/null +++ b/apps/docs/content/docs/tafsir.mdx @@ -0,0 +1,82 @@ +--- +title: Tafsir API +description: Retrieve tafsir text by surah, page, juz, hizb, rub, manzil, ruku, or ayah. +--- + +The Tafsir API gives you direct access to tafsir content in the same style as the rest of the SDK. + +## Get a Single Tafsir + +```ts +const tafsir = await client.tafsir.get("169", { + chapterNumber: 1, +}); + +console.log(tafsir.tafsirs[0]); +``` + +### TafsirResponse Type + + + +## Get Tafsirs by Chapter + +```ts +const surahTafsirs = await client.tafsir.findByChapter("169", "1", { + page: 1, + perPage: 10, +}); +``` + +## Get Tafsirs by Page + +```ts +const pageTafsirs = await client.tafsir.findByPage("169", "1"); +``` + +## Get Tafsirs by Juz + +```ts +const juzTafsirs = await client.tafsir.findByJuz("169", "1"); +``` + +## Get Tafsirs by Hizb + +```ts +const hizbTafsirs = await client.tafsir.findByHizb("169", "1"); +``` + +## Get Tafsirs by Rub El Hizb + +```ts +const rubTafsirs = await client.tafsir.findByRubElHizb("169", "1"); +const rubAlias = await client.tafsir.findByRub("169", "1"); +``` + +## Get Tafsirs by Manzil + +```ts +const manzilTafsirs = await client.tafsir.findByManzil("169", "1"); +``` + +## Get Tafsirs by Ruku + +```ts +const rukuTafsirs = await client.tafsir.findByRuku("169", "1"); +``` + +## Get Tafsirs by Ayah + +```ts +const ayahTafsirs = await client.tafsir.findByAyah("169", "1:1"); +``` + +## Tafsir and Resources + +```ts +const availableTafsirs = await client.resources.findAllTafsirs(); +const tafsirInfo = await client.resources.findTafsirInfo("169"); +``` diff --git a/packages/api/src/runtime/create-client.ts b/packages/api/src/runtime/create-client.ts index cc4cd3b..f04ecc5 100644 --- a/packages/api/src/runtime/create-client.ts +++ b/packages/api/src/runtime/create-client.ts @@ -30,6 +30,7 @@ import { QuranHadithReferences } from "@/sdk/hadith-references"; import { QuranJuzs } from "@/sdk/juzs"; import { QuranResources } from "@/sdk/resources"; import { QuranSearch } from "@/sdk/search"; +import { QuranTafsir } from "@/sdk/tafsir"; import { QuranVerses } from "@/sdk/verses"; type RuntimeClientConfig = PublicClientConfig | ServerClientConfig; @@ -360,6 +361,7 @@ const createContentFacade = ( audio: QuranAudio, hadithReferences: QuranHadithReferences, resources: QuranResources, + tafsir: QuranTafsir, raw: Record, ) => { return { @@ -409,6 +411,28 @@ const createContentFacade = ( list: () => juzs.findAll(), }, raw, + tafsir: { + get: (tafsirId: string | number, query?: ApiParams) => + tafsir.get(tafsirId, query), + byAyah: (resourceId: string | number, verseKey: VerseKey, query?: ApiParams) => + tafsir.findByAyah(resourceId, verseKey, query), + byChapter: (resourceId: string | number, chapterId: ChapterId, query?: ApiParams) => + tafsir.findByChapter(resourceId, chapterId, query), + byHizb: (resourceId: string | number, hizb: HizbNumber, query?: ApiParams) => + tafsir.findByHizb(resourceId, hizb, query), + byJuz: (resourceId: string | number, juz: JuzNumber, query?: ApiParams) => + tafsir.findByJuz(resourceId, juz, query), + byManzil: (resourceId: string | number, manzilNumber: number | string, query?: ApiParams) => + tafsir.findByManzil(resourceId, manzilNumber, query), + byPage: (resourceId: string | number, page: PageNumber, query?: ApiParams) => + tafsir.findByPage(resourceId, page, query), + byRub: (resourceId: string | number, rubNumber: RubNumber, query?: ApiParams) => + tafsir.findByRub(resourceId, rubNumber, query), + byRubElHizb: (resourceId: string | number, rubElHizbNumber: RubNumber, query?: ApiParams) => + tafsir.findByRubElHizb(resourceId, rubElHizbNumber, query), + byRuku: (resourceId: string | number, rukuNumber: number | string, query?: ApiParams) => + tafsir.findByRuku(resourceId, rukuNumber, query), + }, resources: { findSnapshot: ( resourceGroup: ContentSyncResourceGroup, @@ -478,6 +502,7 @@ export const createRuntimeClient = ( const hadithReferences = new QuranHadithReferences(fetcher); const resources = new QuranResources(fetcher); const searchClient = new QuranSearch(fetcher); + const tafsir = new QuranTafsir(fetcher); const raw = { auth: { @@ -505,6 +530,7 @@ export const createRuntimeClient = ( audio, hadithReferences, resources, + tafsir, raw.content.v4, ); const authV1 = createAuthFacade(fetcher); @@ -554,6 +580,7 @@ export const createRuntimeClient = ( raw, resources, search, + tafsir, verses, }; }; diff --git a/packages/api/src/runtime/create-public-client.ts b/packages/api/src/runtime/create-public-client.ts index 8380abb..9677727 100644 --- a/packages/api/src/runtime/create-public-client.ts +++ b/packages/api/src/runtime/create-public-client.ts @@ -361,6 +361,7 @@ export const createPublicRuntimeClient = (config: PublicClientConfig) => { }, chapters: serverOnlyGuard, clearCachedTokens: () => fetcher.clearCachedTokens(), + tafsir: serverOnlyGuard, content: serverOnlyGuard, getUserSession: () => fetcher.getUserSession(), hadithReferences: serverOnlyGuard, diff --git a/packages/api/src/sdk/client.ts b/packages/api/src/sdk/client.ts index 53c0b0c..dfb9c5a 100644 --- a/packages/api/src/sdk/client.ts +++ b/packages/api/src/sdk/client.ts @@ -19,6 +19,7 @@ import { QuranHadithReferences } from "./hadith-references"; import { QuranJuzs } from "./juzs"; import { QuranResources } from "./resources"; import { QuranSearch } from "./search"; +import { QuranTafsir } from "./tafsir"; import { QuranVerses } from "./verses"; const { camelizeKeys } = humps; @@ -138,6 +139,7 @@ export class QuranClient { public readonly hadithReferences: QuranHadithReferences; public readonly resources: QuranResources; public readonly search: QuranSearch; + public readonly tafsir: QuranTafsir; constructor(config: QuranClientConfig) { this.config = { @@ -161,6 +163,7 @@ export class QuranClient { this.hadithReferences = new QuranHadithReferences(this.fetcher); this.resources = new QuranResources(this.fetcher); this.search = new QuranSearch(this.fetcher); + this.tafsir = new QuranTafsir(this.fetcher); } public getConfig(): QuranClientConfig { diff --git a/packages/api/src/sdk/tafsir.ts b/packages/api/src/sdk/tafsir.ts new file mode 100644 index 0000000..53bf9af --- /dev/null +++ b/packages/api/src/sdk/tafsir.ts @@ -0,0 +1,197 @@ +import type { + BaseApiParams, + ChapterId, + HizbNumber, + JuzNumber, + PageNumber, + QuranFetchClient, + RubNumber, + TafsirResponse, + VerseKey, +} from "@/types"; +import { + isValidChapterId, + isValidHizb, + isValidJuz, + isValidQuranPage, + isValidRub, + isValidVerseKey, +} from "@/utils"; + +type GetTafsirOptions = BaseApiParams & { + chapterNumber?: ChapterId; + fields?: string; + page?: number; + perPage?: number; +}; + +/** + * Tafsir API methods + */ +export class QuranTafsir { + constructor(private fetcher: QuranFetchClient) {} + + /** + * Get a single tafsir. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/tafsir/ + * @param {string | number} tafsirId tafsir id + * @param {GetTafsirOptions} options + * @example + * client.tafsir.get('169', { chapterNumber: 1 }) + */ + async get( + tafsirId: string | number, + options?: GetTafsirOptions, + ): Promise { + return this.fetcher.fetch( + `/content/api/v4/quran/tafsirs/${tafsirId}`, + options, + ); + } + + /** + * Get tafsirs for a specific chapter. + * @description https://api-docs.quran.foundation/docs/content_apis_versioned/4.0.0/list-surah-tafsirs/ + * @param {string | number} resourceId tafsir resource id + * @param {ChapterId} chapterNumber chapter id, minimum 1, maximum 114 + * @param {GetTafsirOptions} options + * @example + * client.tafsir.findByChapter('169', '1') + */ + async findByChapter( + resourceId: string | number, + chapterNumber: ChapterId, + options?: GetTafsirOptions, + ): Promise { + if (!isValidChapterId(chapterNumber)) throw new Error("Invalid chapter id"); + + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_chapter/${chapterNumber}`, + options, + ); + } + + /** + * Get tafsirs for a specific page. + */ + async findByPage( + resourceId: string | number, + pageNumber: PageNumber, + options?: GetTafsirOptions, + ): Promise { + if (!isValidQuranPage(pageNumber)) throw new Error("Invalid page number"); + + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_page/${pageNumber}`, + options, + ); + } + + /** + * Get tafsirs for a specific juz. + */ + async findByJuz( + resourceId: string | number, + juzNumber: JuzNumber, + options?: GetTafsirOptions, + ): Promise { + if (!isValidJuz(juzNumber)) throw new Error("Invalid juz"); + + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_juz/${juzNumber}`, + options, + ); + } + + /** + * Get tafsirs for a specific rub el hizb. + */ + async findByRubElHizb( + resourceId: string | number, + rubElHizbNumber: RubNumber, + options?: GetTafsirOptions, + ): Promise { + if (!isValidRub(rubElHizbNumber)) throw new Error("Invalid rub number"); + + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_rub_el_hizb/${rubElHizbNumber}`, + options, + ); + } + + /** + * Alias for rub el hizb. + */ + async findByRub( + resourceId: string | number, + rubNumber: RubNumber, + options?: GetTafsirOptions, + ): Promise { + if (!isValidRub(rubNumber)) throw new Error("Invalid rub number"); + + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_rub/${rubNumber}`, + options, + ); + } + + /** + * Get tafsirs for a specific hizb. + */ + async findByHizb( + resourceId: string | number, + hizbNumber: HizbNumber, + options?: GetTafsirOptions, + ): Promise { + if (!isValidHizb(hizbNumber)) throw new Error("Invalid hizb"); + + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_hizb/${hizbNumber}`, + options, + ); + } + + /** + * Get tafsirs for a specific manzil. + */ + async findByManzil( + resourceId: string | number, + manzilNumber: number | string, + options?: GetTafsirOptions, + ): Promise { + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_manzil/${manzilNumber}`, + options, + ); + } + + /** + * Get tafsirs for a specific ruku. + */ + async findByRuku( + resourceId: string | number, + rukuNumber: number | string, + options?: GetTafsirOptions, + ): Promise { + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_ruku/${rukuNumber}`, + options, + ); + } + + /** + * Get tafsirs for a specific ayah. + */ + async findByAyah( + resourceId: string | number, + verseKey: VerseKey, + options?: GetTafsirOptions, + ): Promise { + if (!isValidVerseKey(verseKey)) throw new Error("Invalid verse key"); + + return this.fetcher.fetch( + `/content/api/v4/tafsirs/${resourceId}/by_ayah/${verseKey}`, + options, + ); + } +} diff --git a/packages/api/src/types/api/TafsirResponse.ts b/packages/api/src/types/api/TafsirResponse.ts new file mode 100644 index 0000000..b15fb10 --- /dev/null +++ b/packages/api/src/types/api/TafsirResponse.ts @@ -0,0 +1,15 @@ +import type { Pagination } from "./Pagination"; +import type { Tafsir } from "./Tafsir"; + +export interface TafsirMeta { + tafsirName?: string; + authorName?: string; +} + +export interface TafsirResponse { + tafsirs: Tafsir[]; + meta?: TafsirMeta; + pagination?: Omit & { + nextPage: number | null; + }; +} diff --git a/packages/api/src/types/api/index.ts b/packages/api/src/types/api/index.ts index bbb2441..c7b0fb7 100644 --- a/packages/api/src/types/api/index.ts +++ b/packages/api/src/types/api/index.ts @@ -10,6 +10,7 @@ export * from './Reciter'; export * from './Segment'; export * from './Tafsir'; export * from './TafsirInfo'; +export * from './TafsirResponse'; export * from './TranslatedName'; export * from './Translation'; export * from './Transliteration'; diff --git a/packages/api/test/public-client.test.ts b/packages/api/test/public-client.test.ts index 0aa86c4..71e7e13 100644 --- a/packages/api/test/public-client.test.ts +++ b/packages/api/test/public-client.test.ts @@ -53,6 +53,7 @@ describe("createPublicClient", () => { }; await expect(content.v4.chapters.list()).rejects.toThrowError(/server/i); + expect(typeof client.tafsir.get).toBe("function"); await expect(answers.findByAyah("2:255")).rejects.toThrowError(/server/i); const response = diff --git a/packages/api/test/server-client.test.ts b/packages/api/test/server-client.test.ts index d498c5c..9eeffcf 100644 --- a/packages/api/test/server-client.test.ts +++ b/packages/api/test/server-client.test.ts @@ -75,6 +75,19 @@ describe("createServerClient", () => { expect(search.pagination.currentPage).toBe(1); }); + it("exposes the tafsir facade through the runtime client", async () => { + const client = createServerClient({ + clientId: "client-id", + clientSecret: "client-secret", + services: { + contentBaseUrl: "http://localhost:3020", + }, + }); + + expect(typeof client.tafsir.get).toBe("function"); + expect(typeof client.content.v4.tafsir.byChapter).toBe("function"); + }); + it("lets server client defaults override the Arabic language default", async () => { let chaptersUrl: string | null = null; diff --git a/packages/api/test/tafsir.test.ts b/packages/api/test/tafsir.test.ts new file mode 100644 index 0000000..774f418 --- /dev/null +++ b/packages/api/test/tafsir.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; + +import { testClient } from "./test-client"; + +const RESOURCE_ID = "169"; + +describe("Tafsir API", () => { + describe("get()", () => { + it("should return a single tafsir payload", async () => { + const response = await testClient.tafsir.get(RESOURCE_ID); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + }); + + describe("findByChapter()", () => { + it("should return tafsirs for a chapter", async () => { + const response = await testClient.tafsir.findByChapter(RESOURCE_ID, "1"); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + + it("should throw for invalid chapter id", async () => { + await expect( + // @ts-expect-error - invalid chapter id + testClient.tafsir.findByChapter(RESOURCE_ID, "0"), + ).rejects.toThrowError(); + }); + }); + + describe("findByPage()", () => { + it("should return tafsirs for a page", async () => { + const response = await testClient.tafsir.findByPage(RESOURCE_ID, "1"); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + }); + + describe("findByJuz()", () => { + it("should return tafsirs for a juz", async () => { + const response = await testClient.tafsir.findByJuz(RESOURCE_ID, "1"); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + }); + + describe("findByHizb()", () => { + it("should return tafsirs for a hizb", async () => { + const response = await testClient.tafsir.findByHizb(RESOURCE_ID, "1"); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + }); + + describe("findByRub()", () => { + it("should return tafsirs for a rub", async () => { + const response = await testClient.tafsir.findByRub(RESOURCE_ID, "1"); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + + it("should throw for invalid rub number", async () => { + await expect( + // @ts-expect-error - invalid rub number + testClient.tafsir.findByRub(RESOURCE_ID, "0"), + ).rejects.toThrowError(); + }); + }); + + describe("findByRubElHizb()", () => { + it("should return tafsirs for a rub el hizb", async () => { + const response = await testClient.tafsir.findByRubElHizb(RESOURCE_ID, "1"); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + }); + + describe("findByAyah()", () => { + it("should return tafsirs for an ayah", async () => { + const response = await testClient.tafsir.findByAyah(RESOURCE_ID, "1:1"); + expect(response.tafsirs).toBeInstanceOf(Array); + }); + + it("should throw for invalid verse key", async () => { + await expect( + // @ts-expect-error - invalid verse key + testClient.tafsir.findByAyah(RESOURCE_ID, "0:0"), + ).rejects.toThrowError(); + }); + }); +});