Skip to content

Commit 277632f

Browse files
authored
Merge pull request #39 from basit3407/chore/search-api-sync-v1
Align SDK search API with v1 search service
2 parents d71eea7 + 211b19b commit 277632f

8 files changed

Lines changed: 310 additions & 75 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ dist
1414
env.d.ts
1515
next-env.d.ts
1616
**/.vscode
17+
18+
.env

packages/api/mocks/handlers.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,34 @@ export const handlers = [
101101
},
102102
),
103103

104-
http.get(
105-
"https://apis.quran.foundation/content/api/v4/search",
106-
({ request }) => {
107-
try {
108-
validateAuth(request);
109-
return HttpResponse.json({});
110-
} catch {
111-
return HttpResponse.text("Unauthorized", { status: 401 });
112-
}
113-
},
114-
),
104+
http.get("https://apis.quran.foundation/v1/search", ({ request }) => {
105+
try {
106+
validateAuth(request);
107+
return HttpResponse.json({
108+
result: {
109+
navigation: [
110+
{
111+
result_type: "surah",
112+
key: "1",
113+
name: "Al-Fatihah",
114+
arabic: "الفاتحة",
115+
isArabic: true,
116+
},
117+
],
118+
verses: [],
119+
},
120+
pagination: {
121+
current_page: 1,
122+
next_page: null,
123+
per_page: 30,
124+
total_pages: 1,
125+
total_records: 1,
126+
},
127+
});
128+
} catch {
129+
return HttpResponse.text("Unauthorized", { status: 401 });
130+
}
131+
}),
115132

116133
http.get(
117134
"https://apis.quran.foundation/content/api/v4/resources/recitations",

packages/api/src/lib/url.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,48 @@
11
import type { ApiParams } from "@/types";
22
import humps from "humps";
33

4-
const { decamelize, decamelizeKeys } = humps;
4+
const { decamelize } = humps;
55

66
export const removeBeginningSlash = (url: string) => {
77
return url.startsWith("/") ? url.slice(1) : url;
88
};
99

10-
const fieldsKey = ["wordFields", "translationFields", "fields"];
10+
const fieldsKey = ["wordFields", "translationFields", "fields"] as const;
11+
const fieldsKeySet = new Set<string>([
12+
...fieldsKey,
13+
...fieldsKey.map((key) => decamelize(key)),
14+
]);
15+
const preservedKeys = new Set(["navigationalResultsNumber", "versesResultsNumber"]);
1116

1217
export const paramsToString = (params?: ApiParams): string => {
1318
if (!params) return "";
1419

15-
const paramsWithDecamelizedKeys = decamelizeKeys(params) as ApiParams;
1620
const paramsString = new URLSearchParams();
1721

18-
for (const [key, value] of Object.entries(paramsWithDecamelizedKeys)) {
19-
if (value === undefined) continue;
20-
21-
if (typeof value === "string") {
22-
paramsString.set(key, value);
23-
} else if (typeof value === "number") {
24-
paramsString.set(key, value.toString());
25-
} else if (typeof value === "boolean") {
26-
paramsString.set(key, value.toString());
27-
} else if (Array.isArray(value)) {
28-
paramsString.set(key, value.join(","));
29-
}
22+
for (const [rawKey, rawValue] of Object.entries(params)) {
23+
if (rawValue === undefined) continue;
24+
25+
const key = preservedKeys.has(rawKey) ? rawKey : decamelize(rawKey);
3026

3127
// fields is a special case, it's an object with boolean values
32-
if (fieldsKey.includes(key)) {
33-
const fields = Object.entries(value)
28+
if (fieldsKeySet.has(rawKey) || fieldsKeySet.has(key)) {
29+
const fields = Object.entries(rawValue as Record<string, boolean>)
3430
.filter(([, value]) => value)
35-
.map(([key]) => decamelize(key));
31+
.map(([fieldKey]) => decamelize(fieldKey));
3632
if (fields.length > 0) {
3733
paramsString.set(key, fields.join(","));
3834
}
35+
continue;
36+
}
37+
38+
if (typeof rawValue === "string") {
39+
paramsString.set(key, rawValue);
40+
} else if (typeof rawValue === "number") {
41+
paramsString.set(key, rawValue.toString());
42+
} else if (typeof rawValue === "boolean") {
43+
paramsString.set(key, rawValue.toString());
44+
} else if (Array.isArray(rawValue)) {
45+
paramsString.set(key, rawValue.join(","));
3946
}
4047
}
4148

packages/api/src/sdk/search.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { SearchParams, SearchResponse } from "@/types";
22

33
import type { QuranFetcher } from "./fetcher";
44

5-
type SearchOptions = SearchParams;
5+
type SearchOptions = Omit<SearchParams, "query">;
66

77
/**
88
* Search API methods
@@ -12,28 +12,20 @@ export class QuranSearch {
1212

1313
/**
1414
* Search
15-
* @description https://api-docs.quran.com/docs/quran.com_versioned/4.0.0/search
16-
* @param {string} q search query
15+
* @description /v1/search
16+
* @param {string} query search query
1717
* @param {SearchOptions} options
1818
* @example
19-
* client.search.search('نور')
20-
* client.search.search('نور', { language: Language.ENGLISH })
21-
* client.search.search('نور', { language: Language.ENGLISH, size: 10 })
22-
* client.search.search('نور', { language: Language.ENGLISH, page: 2 })
19+
* client.search.search('نور', { mode: SearchMode.Quick })
20+
* client.search.search('نور', { mode: SearchMode.Advanced, exactMatchesOnly: '1' })
21+
* client.search.search('نور', { mode: SearchMode.Quick, size: 10 })
22+
* client.search.search('نور', { mode: SearchMode.Quick, page: 2 })
2323
*/
24-
async search(
25-
q: string,
26-
options?: SearchOptions,
27-
): Promise<SearchResponse["search"]> {
28-
const { search } = await this.fetcher.fetch<SearchResponse>(
29-
"/content/api/v4/search",
30-
{
31-
q,
32-
size: 30, // search-specific default
33-
...options,
34-
},
35-
);
36-
37-
return search;
24+
async search(query: string, options: SearchOptions): Promise<SearchResponse> {
25+
return this.fetcher.fetch<SearchResponse>("/v1/search", {
26+
query,
27+
size: 30, // search-specific default
28+
...options,
29+
});
3830
}
3931
}

packages/api/src/types/BaseApiParams.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { Language } from ".";
1+
import type {
2+
Language,
3+
TranslationField,
4+
VerseField,
5+
WordField,
6+
} from ".";
27

38
export type ApiParams = Record<
49
string,
@@ -23,12 +28,47 @@ export interface PaginationParams extends ApiParams {
2328
perPage?: number;
2429
}
2530

31+
export type BinaryString = "0" | "1";
32+
33+
export enum SearchMode {
34+
Advanced = "advanced",
35+
Quick = "quick",
36+
}
37+
2638
/**
2739
* Search parameters
2840
*/
2941
export interface SearchParams extends BaseApiParams {
30-
/** Number of results to return */
31-
size?: number;
42+
/** Search mode */
43+
mode: SearchMode;
44+
/** Search query */
45+
query: string;
46+
/** Filter translations */
47+
filterTranslations?: string | string[];
48+
/** For advanced search, limit to exact matches */
49+
exactMatchesOnly?: BinaryString;
50+
/** Include text in the response */
51+
getText?: BinaryString;
52+
/** Include highlighted text */
53+
highlight?: BinaryString;
54+
/** Quick search navigational results count */
55+
navigationalResultsNumber?: number;
56+
/** Quick search verse results count */
57+
versesResultsNumber?: number;
58+
/** Comma-separated list of indexes */
59+
indexes?: string | string[];
3260
/** Page number for pagination */
3361
page?: number;
62+
/** Number of results to return */
63+
size?: number;
64+
/** Translation IDs to use for language detection */
65+
translationIds?: string | number | Array<string | number>;
66+
/** Quran fields to include in verse filters */
67+
fields?: Partial<Record<VerseField, boolean>>;
68+
/** Translation fields to include in verse filters */
69+
translationFields?: Partial<Record<TranslationField, boolean>>;
70+
/** Word fields to include in verse filters */
71+
wordFields?: Partial<Record<WordField, boolean>>;
72+
/** Include word data in verse filters */
73+
words?: boolean;
3474
}
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
import type { Translation } from "./Translation";
2-
import type { Word } from "./Word";
1+
import type { SearchResult } from "./search-result";
32

43
export interface SearchResponse {
5-
search: {
6-
query: string;
7-
totalResults: number;
4+
pagination: {
85
currentPage: number;
6+
nextPage: number | null;
7+
perPage: number;
98
totalPages: number;
10-
results?: {
11-
verseKey: string;
12-
verse_id: number;
13-
text: string;
14-
highlighted: string;
15-
words: Word[];
16-
translations: Translation[];
17-
}[];
9+
totalRecords: number;
10+
};
11+
result?: {
12+
navigation: SearchResult[];
13+
verses: SearchResult[];
1814
};
1915
}
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import type { VerseKey } from "../common/verse-key";
2-
import type { Translation } from "./Translation";
3-
import type { Word } from "./Word";
1+
export enum SearchNavigationType {
2+
SURAH = "surah",
3+
JUZ = "juz",
4+
HIZB = "hizb",
5+
AYAH = "ayah",
6+
RUB_EL_HIZB = "rub_el_hizb",
7+
SEARCH_PAGE = "search_page",
8+
PAGE = "page",
9+
RANGE = "range",
10+
QURAN_RANGE = "quran_range",
11+
}
412

513
export interface SearchResult {
6-
verseKey: VerseKey;
7-
verseId: number;
8-
text: string;
9-
highlighted?: string;
10-
words: Word[];
11-
translations: Translation[];
14+
resultType: SearchNavigationType;
15+
key: number | string;
16+
name: string;
17+
arabic?: string;
18+
isArabic?: boolean;
19+
isTransliteration?: boolean;
1220
}

0 commit comments

Comments
 (0)