From f1e47d258ff8328ce84b8e83b6349058d1f7b3f4 Mon Sep 17 00:00:00 2001 From: Flow Runner Date: Sun, 17 May 2026 13:11:08 +0000 Subject: [PATCH 1/2] feat(sdk): add tafsir facade add a dedicated tafsir client and content facade so tafsir endpoints can be used directly through the SDK instead of only through resources or raw calls. include docs and tests for the new surface. --- apps/docs/content/docs/tafsir.mdx | 82 ++++++++ packages/api/src/runtime/create-client.ts | 27 +++ .../api/src/runtime/create-public-client.ts | 1 + packages/api/src/sdk/client.ts | 3 + packages/api/src/sdk/tafsir.ts | 190 ++++++++++++++++++ packages/api/src/types/api/TafsirResponse.ts | 15 ++ packages/api/src/types/api/index.ts | 1 + packages/api/test/public-client.test.ts | 1 + packages/api/test/server-client.test.ts | 13 ++ packages/api/test/tafsir.test.ts | 63 ++++++ 10 files changed, 396 insertions(+) create mode 100644 apps/docs/content/docs/tafsir.mdx create mode 100644 packages/api/src/sdk/tafsir.ts create mode 100644 packages/api/src/types/api/TafsirResponse.ts create mode 100644 packages/api/test/tafsir.test.ts diff --git a/apps/docs/content/docs/tafsir.mdx b/apps/docs/content/docs/tafsir.mdx new file mode 100644 index 0000000..d226d9a --- /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", { + chapter_number: 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..7b46161 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: number | string, query?: ApiParams) => + tafsir.findByRub(resourceId, rubNumber, query), + byRubElHizb: (resourceId: string | number, rubElHizbNumber: number | string, 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..7c2d16b --- /dev/null +++ b/packages/api/src/sdk/tafsir.ts @@ -0,0 +1,190 @@ +import type { + BaseApiParams, + ChapterId, + HizbNumber, + JuzNumber, + PageNumber, + QuranFetchClient, + TafsirResponse, + VerseKey, +} from "@/types"; +import { + isValidChapterId, + isValidHizb, + isValidJuz, + isValidQuranPage, + isValidVerseKey, +} from "@/utils"; + +type GetTafsirOptions = BaseApiParams & { + 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: number | string, + options?: GetTafsirOptions, + ): Promise { + 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: number | string, + options?: GetTafsirOptions, + ): Promise { + 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..a794a6e --- /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?: Pagination & { + 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..fa5c872 --- /dev/null +++ b/packages/api/test/tafsir.test.ts @@ -0,0 +1,63 @@ +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("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(); + }); + }); +}); From 1963f0e626b941861ca90ee19e450c0fec59d6dd Mon Sep 17 00:00:00 2001 From: Flow Runner Date: Thu, 21 May 2026 05:26:21 +0000 Subject: [PATCH 2/2] fix(sdk): address tafsir review feedback --- apps/docs/content/docs/tafsir.mdx | 2 +- packages/api/src/runtime/create-client.ts | 4 ++-- packages/api/src/sdk/tafsir.ts | 11 ++++++++-- packages/api/src/types/api/TafsirResponse.ts | 2 +- packages/api/test/tafsir.test.ts | 21 ++++++++++++++++++++ 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/docs/content/docs/tafsir.mdx b/apps/docs/content/docs/tafsir.mdx index d226d9a..4abbf44 100644 --- a/apps/docs/content/docs/tafsir.mdx +++ b/apps/docs/content/docs/tafsir.mdx @@ -9,7 +9,7 @@ The Tafsir API gives you direct access to tafsir content in the same style as th ```ts const tafsir = await client.tafsir.get("169", { - chapter_number: 1, + chapterNumber: 1, }); console.log(tafsir.tafsirs[0]); diff --git a/packages/api/src/runtime/create-client.ts b/packages/api/src/runtime/create-client.ts index 7b46161..f04ecc5 100644 --- a/packages/api/src/runtime/create-client.ts +++ b/packages/api/src/runtime/create-client.ts @@ -426,9 +426,9 @@ const createContentFacade = ( tafsir.findByManzil(resourceId, manzilNumber, query), byPage: (resourceId: string | number, page: PageNumber, query?: ApiParams) => tafsir.findByPage(resourceId, page, query), - byRub: (resourceId: string | number, rubNumber: number | string, query?: ApiParams) => + byRub: (resourceId: string | number, rubNumber: RubNumber, query?: ApiParams) => tafsir.findByRub(resourceId, rubNumber, query), - byRubElHizb: (resourceId: string | number, rubElHizbNumber: number | string, query?: ApiParams) => + 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), diff --git a/packages/api/src/sdk/tafsir.ts b/packages/api/src/sdk/tafsir.ts index 7c2d16b..53bf9af 100644 --- a/packages/api/src/sdk/tafsir.ts +++ b/packages/api/src/sdk/tafsir.ts @@ -5,6 +5,7 @@ import type { JuzNumber, PageNumber, QuranFetchClient, + RubNumber, TafsirResponse, VerseKey, } from "@/types"; @@ -13,10 +14,12 @@ import { isValidHizb, isValidJuz, isValidQuranPage, + isValidRub, isValidVerseKey, } from "@/utils"; type GetTafsirOptions = BaseApiParams & { + chapterNumber?: ChapterId; fields?: string; page?: number; perPage?: number; @@ -105,9 +108,11 @@ export class QuranTafsir { */ async findByRubElHizb( resourceId: string | number, - rubElHizbNumber: number | string, + 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, @@ -119,9 +124,11 @@ export class QuranTafsir { */ async findByRub( resourceId: string | number, - rubNumber: number | string, + 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, diff --git a/packages/api/src/types/api/TafsirResponse.ts b/packages/api/src/types/api/TafsirResponse.ts index a794a6e..b15fb10 100644 --- a/packages/api/src/types/api/TafsirResponse.ts +++ b/packages/api/src/types/api/TafsirResponse.ts @@ -9,7 +9,7 @@ export interface TafsirMeta { export interface TafsirResponse { tafsirs: Tafsir[]; meta?: TafsirMeta; - pagination?: Pagination & { + pagination?: Omit & { nextPage: number | null; }; } diff --git a/packages/api/test/tafsir.test.ts b/packages/api/test/tafsir.test.ts index fa5c872..774f418 100644 --- a/packages/api/test/tafsir.test.ts +++ b/packages/api/test/tafsir.test.ts @@ -47,6 +47,27 @@ describe("Tafsir API", () => { }); }); + 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");