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