Skip to content

Commit 02e1539

Browse files
Merge pull request #3 from serpapi/support-pagination
Support pagination for `getJson`
2 parents 55b35b8 + 0ad1431 commit 02e1539

14 files changed

Lines changed: 1834 additions & 10 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to
1010

1111
### Added
1212

13+
- Add pagination support for `getJson`.
1314
- Export error classes.
1415
- Add examples.
1516
- [Apple Reviews] Add more sort options.

README.md

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { getJson } from "https://deno.land/x/serpapi/mod.ts";
4949
- Promises and async/await support.
5050
- Callbacks support.
5151
- [Examples in JavaScript/TypeScript on Node.js/Deno using ESM/CommonJS, and more](https://github.com/serpapi/serpapi-javascript/tree/master/examples).
52-
- (Planned) Pagination support.
52+
- [Pagination support](#pagination).
5353
- (Planned) More error classes.
5454

5555
## Configuration
@@ -72,6 +72,33 @@ await getJson("google", { q: "coffee" }); // uses the API key defined in the con
7272
await getJson("google", { api_key: API_KEY_2, q: "coffee" }); // API_KEY_2 will be used
7373
```
7474

75+
## Pagination
76+
77+
Search engines handle pagination in several different ways. Some rely on an
78+
"offset" value to return results starting from a specific index, while some
79+
others rely on the typical notion of a "page". These are often combined with a
80+
"size" value to define how many results are returned in a search.
81+
82+
This module helps you handle pagination easily. After receiving search results
83+
from `getJson`, simply call the `next()` method on the returned object to
84+
retrieve the next page of results. If there is no `next()` method, then either
85+
pagination is not supported for the search engine or there are no more pages to
86+
be retrieved.
87+
88+
```js
89+
const page1 = await getJson("google", { q: "coffee", start: 15 });
90+
const page2 = await page1.next?.();
91+
```
92+
93+
You may pass in the engine's supported pagination parameters as per normal. In
94+
the above example, the first page contains the 15th to the 24th result while the
95+
second page contains the 25th to the 34th result.
96+
97+
Note that if you set `no_cache` to `true`, all subsequent `next()` calls will
98+
not return cached results.
99+
100+
Refer to the [`getJson` definition below](#getjson) for more examples.
101+
75102
## Functions
76103
77104
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
@@ -102,6 +129,8 @@ await getJson("google", { api_key: API_KEY_2, q: "coffee" }); // API_KEY_2 will
102129
Get a JSON response based on search parameters.
103130
104131
- Accepts an optional callback.
132+
- Get the next page of results by calling the `.next()` method on the returned
133+
response object.
105134
106135
#### Parameters
107136
@@ -116,13 +145,50 @@ Get a JSON response based on search parameters.
116145
#### Examples
117146
118147
```javascript
119-
// async/await
148+
// single call (async/await)
120149
const json = await getJson("google", { api_key: API_KEY, q: "coffee" });
121150

122-
// callback
151+
// single call (callback)
123152
getJson("google", { api_key: API_KEY, q: "coffee" }, console.log);
124153
```
125154
155+
```javascript
156+
// pagination (async/await)
157+
const page1 = await getJson("google", { q: "coffee", start: 15 });
158+
const page2 = await page1.next?.();
159+
```
160+
161+
```javascript
162+
// pagination (callback)
163+
getJson("google", { q: "coffee", start: 15 }, (page1) => {
164+
page1.next?.((page2) => {
165+
console.log(page2);
166+
});
167+
});
168+
```
169+
170+
```javascript
171+
// pagination loop (async/await)
172+
const organicResults = [];
173+
let page = await getJson("google", { api_key: API_KEY, q: "coffee" });
174+
while (page) {
175+
organicResults.push(...page.organic_results);
176+
if (organicResults.length >= 30) break;
177+
page = await page.next?.();
178+
}
179+
```
180+
181+
```javascript
182+
// pagination loop (callback)
183+
const organicResults = [];
184+
getJson("google", { api_key: API_KEY, q: "coffee" }, (page) => {
185+
organicResults.push(...page.organic_results);
186+
if (organicResults.length < 30 && page.next) {
187+
page.next();
188+
}
189+
});
190+
```
191+
126192
### getHtml
127193
128194
Get a HTML response based on search parameters.

src/serpapi.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
LocationsApiParameters,
88
} from "./types.ts";
99
import { EngineMap } from "./engines/engine_map.ts";
10-
import { _internals, execute } from "./utils.ts";
10+
import {
11+
_internals,
12+
execute,
13+
extractNextParameters,
14+
haveParametersChanged,
15+
} from "./utils.ts";
1116
import { validateApiKey, validateTimeout } from "./validators.ts";
1217

1318
const ACCOUNT_PATH = "/account";
@@ -18,24 +23,57 @@ const SEARCH_ARCHIVE_PATH = `/searches`;
1823
/**
1924
* Get a JSON response based on search parameters.
2025
* - Accepts an optional callback.
26+
* - Get the next page of results by calling the `.next()` method on the returned response object.
2127
*
2228
* @param {string} engine - engine name
2329
* @param {object} parameters - search query parameters for the engine
2430
* @param {fn=} callback - optional callback
2531
* @example
26-
* // async/await
32+
* // single call (async/await)
2733
* const json = await getJson("google", { api_key: API_KEY, q: "coffee" });
2834
*
29-
* // callback
35+
* // single call (callback)
3036
* getJson("google", { api_key: API_KEY, q: "coffee" }, console.log);
37+
*
38+
* @example
39+
* // pagination (async/await)
40+
* const page1 = await getJson("google", { q: "coffee", start: 15 });
41+
* const page2 = await page1.next?.();
42+
*
43+
* @example
44+
* // pagination (callback)
45+
* getJson("google", { q: "coffee", start: 15 }, (page1) => {
46+
* page1.next?.((page2) => {
47+
* console.log(page2);
48+
* });
49+
* });
50+
*
51+
* @example
52+
* // pagination loop (async/await)
53+
* const organicResults = [];
54+
* let page = await getJson("google", { api_key: API_KEY, q: "coffee" });
55+
* while (page) {
56+
* organicResults.push(...page.organic_results);
57+
* if (organicResults.length >= 30) break;
58+
* page = await page.next?.();
59+
* }
60+
*
61+
* @example
62+
* // pagination loop (callback)
63+
* const organicResults = [];
64+
* getJson("google", { api_key: API_KEY, q: "coffee" }, (page) => {
65+
* organicResults.push(...page.organic_results);
66+
* if (organicResults.length < 30 && page.next) {
67+
* page.next();
68+
* }
69+
* });
3170
*/
3271
export async function getJson<
3372
E extends keyof EngineMap,
34-
R extends BaseResponse<EngineMap[E]["parameters"]>,
3573
>(
3674
engine: E,
3775
parameters: EngineMap[E]["parameters"],
38-
callback?: (json: R) => void,
76+
callback?: (json: BaseResponse<EngineMap[E]["parameters"]>) => void,
3977
) {
4078
const key = validateApiKey(parameters.api_key, true);
4179
const timeout = validateTimeout(parameters.timeout);
@@ -49,7 +87,26 @@ export async function getJson<
4987
},
5088
timeout,
5189
);
52-
const json = await response.json() as R;
90+
const json = await response.json() as BaseResponse<
91+
EngineMap[E]["parameters"]
92+
>;
93+
const nextParametersFromResponse = extractNextParameters<E>(json);
94+
if (
95+
// https://github.com/serpapi/public-roadmap/issues/562
96+
// https://github.com/serpapi/public-roadmap/issues/563
97+
engine !== "yahoo_shopping" &&
98+
nextParametersFromResponse
99+
) {
100+
const nextParameters = { ...parameters, ...nextParametersFromResponse };
101+
if (haveParametersChanged(parameters, nextParameters)) {
102+
json.next = (innerCallback = callback) =>
103+
getJson(
104+
engine,
105+
nextParameters,
106+
innerCallback,
107+
);
108+
}
109+
}
53110
callback?.(json);
54111
return json;
55112
}

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ export type BaseResponse<P = Record<string | number | symbol, never>> = {
5252
search_parameters:
5353
& { engine: string }
5454
& Omit<BaseParameters & P, "api_key" | "no_cache" | "async" | "timeout">;
55+
serpapi_pagination?: { next: string };
56+
pagination?: { next: string };
57+
next?: (
58+
callback?: (json: BaseResponse<P>) => void,
59+
) => Promise<BaseResponse<P>>;
5560
// deno-lint-ignore no-explicit-any
5661
[key: string]: any; // TODO(seb): use recursive type
5762
};

src/utils.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { EngineMap } from "./engines/engine_map.ts";
12
import { version } from "../version.ts";
23

34
type UrlParameters = Record<
@@ -34,6 +35,44 @@ function getBaseUrl() {
3435
return "https://serpapi.com";
3536
}
3637

38+
type NextParametersKeys<E extends keyof EngineMap> = Omit<
39+
EngineMap[E]["parameters"],
40+
"api_key" | "no_cache" | "async" | "timeout"
41+
>;
42+
type NextParameters<E extends keyof EngineMap> = {
43+
[K in keyof NextParametersKeys<E>]: string;
44+
};
45+
export function extractNextParameters<
46+
E extends keyof EngineMap,
47+
>(json: {
48+
serpapi_pagination?: { next: string };
49+
pagination?: { next: string };
50+
}) {
51+
const nextUrlString = json["serpapi_pagination"]?.["next"] ||
52+
json["pagination"]?.["next"];
53+
54+
if (nextUrlString) {
55+
const nextUrl = new URL(nextUrlString);
56+
const nextParameters = Object.fromEntries(nextUrl.searchParams.entries());
57+
delete nextParameters["engine"];
58+
return nextParameters as NextParameters<E>;
59+
}
60+
}
61+
62+
export function haveParametersChanged(
63+
parameters: Record<string, unknown>,
64+
nextParameters: Record<string, unknown>,
65+
) {
66+
const keys = [
67+
...Object.keys(parameters),
68+
...Object.keys(nextParameters),
69+
];
70+
const uniqueKeys = new Set(keys);
71+
return [...uniqueKeys].some((key) =>
72+
`${parameters[key]}` !== `${nextParameters[key]}` // string comparison
73+
);
74+
}
75+
3776
function getSource() {
3877
const moduleSource = `serpapi@${version}`;
3978
try {

0 commit comments

Comments
 (0)