@crawlora-org/sdk 1.3.0-sdk.1 → 1.5.0-sdk.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.5.0-sdk.1
4
+
5
+ - Added `beforeRequest`/`afterResponse` middleware hooks (mutate the outgoing
6
+ request context, transform the parsed response).
7
+ - Added opt-in `idempotencyKeys` (stable `Idempotency-Key` on POST/PATCH, reused
8
+ across retries) and per-request `retries`/`isRetryable` overrides.
9
+ - Added client-side `rateLimit` (requests/sec) and `maxConcurrency` throttling.
10
+ - Errors now carry `requestId`.
11
+
12
+ ## v1.4.0-sdk.1
13
+
14
+ - Configurable retries: `maxRetryDelay`, `retryStatuses`, and an `isRetryable`
15
+ predicate; added an `onRetry` hook, opt-in `requestId` (x-request-id, also on
16
+ `error.requestId`), and a `logger` event sink.
17
+ - Pagination: cursor/token mode (`cursorParam` + `nextCursor`) and a
18
+ `paginateItems` per-item async iterator; operation metadata now exposes
19
+ `paginatable` and `cursorParams`.
20
+ - Streaming: `responseType: "stream"` returns the raw `Response` on success.
21
+ - Config: `CRAWLORA_API_KEY` / `CRAWLORA_BASE_URL` environment fallback.
22
+
3
23
  ## v1.3.0-sdk.1
4
24
 
5
25
  - Added `CrawloraClientError`, `CrawloraServerError`, and `CrawloraNetworkError`
package/docs/recipes.md CHANGED
@@ -74,6 +74,77 @@ try {
74
74
  }
75
75
  ```
76
76
 
77
+ ## Custom Retries And Observability
78
+
79
+ ```js
80
+ const crawlora = new CrawloraClient({
81
+ retries: 3,
82
+ maxRetryDelay: 10000,
83
+ retryStatuses: [429, 503], // or:
84
+ isRetryable: (status, error) => status >= 500,
85
+ onRetry: (attempt, error, delay) => console.warn(`retry ${attempt} after ${delay}ms`, error.status),
86
+ requestId: true, // sets x-request-id; available as error.requestId
87
+ logger: (event) => console.debug(event)
88
+ });
89
+ ```
90
+
91
+ Branch on `CrawloraClientError` (4xx), `CrawloraServerError` (5xx), and
92
+ `CrawloraNetworkError` (transport).
93
+
94
+ ## Pagination
95
+
96
+ ```js
97
+ // page/offset (auto-detected)
98
+ for await (const page of crawlora.paginate("ebay-seller-feedback", { seller: "acme" })) { /* ... */ }
99
+
100
+ // per-item iteration
101
+ for await (const item of crawlora.paginateItems("ebay-seller-feedback", { seller: "acme" })) { /* ... */ }
102
+
103
+ // cursor/token pagination
104
+ for await (const page of crawlora.paginate("producthunt-leaderboard", {}, {
105
+ cursorParam: "cursor",
106
+ nextCursor: (p) => p.next_cursor
107
+ })) { /* ... */ }
108
+ ```
109
+
110
+ ## Streaming Responses
111
+
112
+ ```js
113
+ const res = await crawlora.request("bing-search", { q: "coffee" }, { responseType: "stream" });
114
+ for await (const chunk of res.body) { /* ... */ }
115
+ ```
116
+
117
+ ## Environment Variables
118
+
119
+ `CRAWLORA_API_KEY` and `CRAWLORA_BASE_URL` are used when not set explicitly
120
+ (precedence: option > env > default).
121
+
122
+ ## Middleware
123
+
124
+ ```js
125
+ const crawlora = new CrawloraClient({
126
+ beforeRequest: (ctx) => { ctx.headers["x-signature"] = sign(ctx); },
127
+ afterResponse: (operationId, status, headers, body) => body // return a value to transform
128
+ });
129
+ ```
130
+
131
+ ## Idempotency And Per-Request Retries
132
+
133
+ ```js
134
+ const crawlora = new CrawloraClient({ idempotencyKeys: true }); // stable key on POST/PATCH retries
135
+
136
+ await crawlora.request("bing-search", { q: "coffee" }, {
137
+ retries: 5,
138
+ isRetryable: (status) => status >= 500
139
+ });
140
+ ```
141
+
142
+ ## Rate Limiting
143
+
144
+ ```js
145
+ const crawlora = new CrawloraClient({ rateLimit: 10, maxConcurrency: 4 });
146
+ ```
147
+
77
148
  ## Optional Live Smoke Tests
78
149
 
79
150
  ```sh
@@ -0,0 +1,22 @@
1
+ import { CrawloraClient } from "../src/index.js";
2
+
3
+ const client = newClient();
4
+ if (client) {
5
+ const seller = process.env.CRAWLORA_EBAY_SELLER || "garlandcomputer";
6
+ let count = 0;
7
+ for await (const item of client.paginateItems("ebay-seller-feedback", { seller }, { maxPages: 3 })) {
8
+ count += 1;
9
+ }
10
+ console.log(`collected ${count} feedback items across up to 3 pages`);
11
+ }
12
+
13
+ function newClient() {
14
+ if (!process.env.CRAWLORA_API_KEY) {
15
+ console.error("set CRAWLORA_API_KEY to run this live example");
16
+ return null;
17
+ }
18
+ return new CrawloraClient({
19
+ apiKey: process.env.CRAWLORA_API_KEY,
20
+ baseUrl: process.env.CRAWLORA_BASE_URL
21
+ });
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlora-org/sdk",
3
- "version": "1.3.0-sdk.1",
3
+ "version": "1.5.0-sdk.2",
4
4
  "description": "JavaScript and TypeScript SDK for the public Crawlora API.",
5
5
  "keywords": [
6
6
  "crawlora",
package/src/client.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { operations, groups } from "./operations.js";
2
2
 
3
3
  const DEFAULT_BASE_URL = "https://api.crawlora.net/api/v1";
4
- export const VERSION = "1.3.0-sdk.1";
4
+ export const VERSION = "1.5.0-sdk.2";
5
5
  const DEFAULT_USER_AGENT = `crawlora-js-sdk/${VERSION}`;
6
6
 
7
7
  export class CrawloraError extends Error {
8
- constructor(message, { status = 0, code, body, headers = {}, response, cause, retryable } = {}) {
8
+ constructor(message, { status = 0, code, body, headers = {}, response, cause, retryable, requestId } = {}) {
9
9
  super(message);
10
10
  this.name = "CrawloraError";
11
11
  this.status = status;
@@ -15,6 +15,7 @@ export class CrawloraError extends Error {
15
15
  this.response = response;
16
16
  this.cause = cause;
17
17
  this.retryable = retryable;
18
+ this.requestId = requestId;
18
19
  }
19
20
  }
20
21
 
@@ -47,14 +48,37 @@ function apiErrorClass(status) {
47
48
  return status >= 500 ? CrawloraServerError : CrawloraClientError;
48
49
  }
49
50
 
51
+ /**
52
+ * Client for the Crawlora API.
53
+ *
54
+ * Call operations via grouped helpers (`client.bing.search({ q })`) or
55
+ * dynamically (`client.request("bing-search", { q })`). Supports configurable
56
+ * retries, an `onRetry` hook, opt-in `requestId` and `idempotencyKeys`,
57
+ * `beforeRequest`/`afterResponse` middleware, client-side `rateLimit` /
58
+ * `maxConcurrency`, pagination (`paginate`/`paginateItems`), and
59
+ * `responseType: "stream"`.
60
+ */
50
61
  export class CrawloraClient {
51
62
  constructor(options = {}) {
52
- this.apiKey = options.apiKey || "";
63
+ // Precedence: explicit option > environment variable > default.
64
+ this.apiKey = options.apiKey || envVar("CRAWLORA_API_KEY") || "";
53
65
  this.jwtToken = options.jwtToken || "";
54
- this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
66
+ this.baseUrl = (options.baseUrl || envVar("CRAWLORA_BASE_URL") || DEFAULT_BASE_URL).replace(/\/+$/, "");
55
67
  this.timeout = options.timeout ?? 30000;
56
68
  this.retries = normalizeNonNegativeInteger(options.retries ?? 0);
57
69
  this.retryDelay = normalizeNonNegativeNumber(options.retryDelay ?? 250);
70
+ this.maxRetryDelay = normalizeNonNegativeNumber(options.maxRetryDelay ?? 30000);
71
+ this.retryStatuses = options.retryStatuses ? new Set(options.retryStatuses) : null;
72
+ this.isRetryable = typeof options.isRetryable === "function" ? options.isRetryable : null;
73
+ this.onRetry = typeof options.onRetry === "function" ? options.onRetry : null;
74
+ this.requestId = options.requestId === true;
75
+ this.idempotencyKeys = options.idempotencyKeys === true;
76
+ this.logger = typeof options.logger === "function" ? options.logger : null;
77
+ this.beforeRequest = asHookList(options.beforeRequest);
78
+ this.afterResponse = asHookList(options.afterResponse);
79
+ this.limiter = (options.rateLimit > 0 || options.maxConcurrency > 0)
80
+ ? new RateLimiter(options.rateLimit || 0, options.maxConcurrency || 0)
81
+ : null;
58
82
  this.headers = { ...(options.headers || {}) };
59
83
  this.userAgent = options.userAgent === false ? "" : (options.userAgent || DEFAULT_USER_AGENT);
60
84
  this.fetch = options.fetch || globalThis.fetch;
@@ -84,22 +108,47 @@ export class CrawloraClient {
84
108
  params = params ?? {};
85
109
  options = options ?? {};
86
110
  const responseType = validateResponseType(options.responseType || "auto");
111
+ this.#log({ event: "request", operation: operationId });
112
+ const maxRetries = options.retries ?? this.retries;
113
+ const isRetryable = typeof options.isRetryable === "function" ? options.isRetryable : null;
114
+ const idempotencyKey = (this.idempotencyKeys && (operation.method === "POST" || operation.method === "PATCH"))
115
+ ? generateId() : undefined;
87
116
 
88
117
  let attempt = 0;
89
118
  for (;;) {
90
119
  try {
91
- return await this.#send(operation, params, options, responseType);
120
+ const run = () => this.#send(operation, params, options, responseType, idempotencyKey);
121
+ return await (this.limiter ? this.limiter.run(run, options.signal) : run());
92
122
  } catch (error) {
93
- if (!(error instanceof CrawloraError) || error.retryable === false || attempt >= this.retries || !shouldRetry(error.status)) {
123
+ const retryable = isRetryable ? !!isRetryable(error.status, error) : this.#isRetryable(error.status, error);
124
+ if (!(error instanceof CrawloraError) || error.retryable === false || attempt >= maxRetries || !retryable) {
94
125
  throw error;
95
126
  }
96
127
  attempt++;
97
- await sleep(retryDelayForError(error, this.retryDelay, attempt), options.signal);
128
+ const delay = this.#retryDelayFor(error, attempt);
129
+ this.#log({ event: "retry", operation: operationId, attempt, status: error.status, delay });
130
+ if (this.onRetry) this.onRetry(attempt, error, delay);
131
+ await sleep(delay, options.signal);
98
132
  }
99
133
  }
100
134
  }
101
135
 
102
- async #send(operation, params, options, responseType) {
136
+ #isRetryable(status, error) {
137
+ if (this.isRetryable) return !!this.isRetryable(status, error);
138
+ if (this.retryStatuses) return status === 0 || this.retryStatuses.has(status);
139
+ return shouldRetry(status);
140
+ }
141
+
142
+ #retryDelayFor(error, attempt) {
143
+ const retryAfter = parseRetryAfter(error.headers, this.maxRetryDelay);
144
+ return retryAfter ?? retryDelay(this.retryDelay, attempt);
145
+ }
146
+
147
+ #log(event) {
148
+ if (this.logger) this.logger(event);
149
+ }
150
+
151
+ async #send(operation, params, options, responseType, idempotencyKey) {
103
152
  const { url, body, bodyHeaders } = buildRequest(operation, this.baseUrl, params);
104
153
  const headers = mergeHeaders(
105
154
  this.headers,
@@ -108,6 +157,19 @@ export class CrawloraClient {
108
157
  bodyHeaders,
109
158
  options.headers
110
159
  );
160
+ const requestId = this.requestId ? ensureRequestId(headers) : (headerValue(headers, "x-request-id") || undefined);
161
+ if (idempotencyKey && !headerValue(headers, "idempotency-key")) {
162
+ headers["Idempotency-Key"] = idempotencyKey;
163
+ }
164
+
165
+ let requestUrl = url;
166
+ let requestHeaders = headers;
167
+ if (this.beforeRequest.length) {
168
+ const ctx = { operationId: operation.id, method: operation.method, url: requestUrl, headers: requestHeaders };
169
+ for (const hook of this.beforeRequest) await hook(ctx);
170
+ requestUrl = ctx.url;
171
+ requestHeaders = ctx.headers;
172
+ }
111
173
 
112
174
  const controller = new AbortController();
113
175
  const timeoutMs = options.timeout ?? this.timeout;
@@ -119,26 +181,30 @@ export class CrawloraClient {
119
181
  const signal = composeSignal(controller, options.signal);
120
182
  let response;
121
183
  try {
122
- response = await this.fetch(url, {
184
+ response = await this.fetch(requestUrl, {
123
185
  method: operation.method,
124
- headers,
186
+ headers: requestHeaders,
125
187
  body,
126
188
  signal
127
189
  });
128
190
  } catch (error) {
129
191
  if (options.signal?.aborted) {
130
- throw new CrawloraNetworkError("Crawlora request aborted", { cause: error, retryable: false });
192
+ throw new CrawloraNetworkError("Crawlora request aborted", { cause: error, retryable: false, requestId });
131
193
  }
132
194
  if (timedOut) {
133
- throw new CrawloraNetworkError("Crawlora request timed out", { cause: error, retryable: false });
195
+ throw new CrawloraNetworkError("Crawlora request timed out", { cause: error, retryable: false, requestId });
134
196
  }
135
- throw new CrawloraNetworkError("Crawlora transport error", { cause: error });
197
+ throw new CrawloraNetworkError("Crawlora transport error", { cause: error, requestId });
136
198
  } finally {
137
199
  if (timeout) clearTimeout(timeout);
138
200
  }
139
201
 
140
202
  const headersObject = responseHeaders(response);
141
- const parsed = await parseResponse(response, responseType, headersObject);
203
+ // Streaming success returns the raw Response; the caller reads response.body.
204
+ if (responseType === "stream" && response.ok) {
205
+ return response;
206
+ }
207
+ let parsed = await parseResponse(response, responseType === "stream" ? "auto" : responseType, headersObject, requestId);
142
208
  if (!response.ok) {
143
209
  const code = parsed && typeof parsed === "object" ? parsed.code : undefined;
144
210
  const message = parsed && typeof parsed === "object" && parsed.msg ? parsed.msg : response.statusText;
@@ -148,26 +214,54 @@ export class CrawloraClient {
148
214
  code,
149
215
  body: parsed,
150
216
  headers: headersObject,
151
- response
217
+ response,
218
+ requestId
152
219
  });
153
220
  }
221
+ if (this.afterResponse.length) {
222
+ for (const hook of this.afterResponse) {
223
+ const result = await hook(operation.id, response.status, headersObject, parsed);
224
+ if (result !== undefined) parsed = result;
225
+ }
226
+ }
154
227
  return parsed;
155
228
  }
156
229
 
157
- // Async iterator over pages of a paginated operation. Advances the numeric
158
- // page/offset query parameter and stops when a page returns no data.
230
+ // Async iterator over pages of a paginated operation. Numeric mode advances
231
+ // the page/offset query parameter and stops on an empty page; cursor mode
232
+ // (cursorParam + nextCursor) sends the cursor and stops when nextCursor is falsy.
159
233
  // for await (const page of client.paginate("ebay-seller-feedback", { seller })) { ... }
160
234
  async *paginate(operationId, params = {}, options = {}) {
161
235
  const operation = operations[operationId];
162
236
  if (!operation) {
163
237
  throw new TypeError(`Unknown Crawlora operation: ${operationId}`);
164
238
  }
239
+ const maxPages = options.maxPages ?? Infinity;
240
+
241
+ if (options.cursorParam || options.nextCursor) {
242
+ if (!(options.cursorParam && options.nextCursor)) {
243
+ throw new TypeError("cursor pagination requires both cursorParam and nextCursor");
244
+ }
245
+ if (!operation.queryParams.some((parameter) => parameter.name === options.cursorParam)) {
246
+ throw new TypeError(`cursorParam ${options.cursorParam} is not a query parameter of ${operationId}`);
247
+ }
248
+ let cursor = options.start;
249
+ for (let i = 0; i < maxPages; i++) {
250
+ const pageParams = { ...params };
251
+ if (cursor !== undefined && cursor !== null) pageParams[options.cursorParam] = cursor;
252
+ const response = await this.request(operationId, pageParams, options);
253
+ yield response;
254
+ cursor = options.nextCursor(response);
255
+ if (!cursor) break;
256
+ }
257
+ return;
258
+ }
259
+
165
260
  const pageParam = options.pageParam || detectPageParam(operation);
166
261
  if (!pageParam) {
167
262
  throw new TypeError(`Operation ${operationId} has no page or offset query parameter to paginate`);
168
263
  }
169
264
  const step = options.step ?? 1;
170
- const maxPages = options.maxPages ?? Infinity;
171
265
  let pageValue = options.start ?? (pageParam === "offset" ? 0 : 1);
172
266
  for (let i = 0; i < maxPages; i++) {
173
267
  const response = await this.request(operationId, { ...params, [pageParam]: pageValue }, options);
@@ -176,10 +270,25 @@ export class CrawloraClient {
176
270
  pageValue += step;
177
271
  }
178
272
  }
273
+
274
+ // Async iterator over individual items across pages. `items` extracts the list
275
+ // from a page (default: the Crawlora `data` array).
276
+ async *paginateItems(operationId, params = {}, options = {}) {
277
+ const extract = options.items || defaultItems;
278
+ for await (const page of this.paginate(operationId, params, options)) {
279
+ for (const item of extract(page)) yield item;
280
+ }
281
+ }
179
282
  }
180
283
 
181
284
  const PAGE_PARAM_NAMES = ["page", "offset"];
182
285
 
286
+ function defaultItems(response) {
287
+ if (response && typeof response === "object" && Array.isArray(response.data)) return response.data;
288
+ if (Array.isArray(response)) return response;
289
+ return [];
290
+ }
291
+
183
292
  function detectPageParam(operation) {
184
293
  for (const name of PAGE_PARAM_NAMES) {
185
294
  if (operation.queryParams.some((parameter) => parameter.name === name)) return name;
@@ -316,13 +425,13 @@ function composeSignal(controller, signal) {
316
425
  }
317
426
 
318
427
  function validateResponseType(responseType) {
319
- if (responseType === "auto" || responseType === "json" || responseType === "text") {
428
+ if (responseType === "auto" || responseType === "json" || responseType === "text" || responseType === "stream") {
320
429
  return responseType;
321
430
  }
322
- throw new TypeError("Invalid responseType: expected one of auto, json, text");
431
+ throw new TypeError("Invalid responseType: expected one of auto, json, text, stream");
323
432
  }
324
433
 
325
- async function parseResponse(response, responseType, headers) {
434
+ async function parseResponse(response, responseType, headers, requestId) {
326
435
  if (responseType === "text") return response.text();
327
436
  const contentType = response.headers.get("content-type") || "";
328
437
  if (responseType === "json" || contentType.toLowerCase().includes("application/json")) {
@@ -335,7 +444,8 @@ async function parseResponse(response, responseType, headers) {
335
444
  body: text,
336
445
  headers,
337
446
  response,
338
- cause: error
447
+ cause: error,
448
+ requestId
339
449
  });
340
450
  }
341
451
  }
@@ -353,25 +463,86 @@ function retryDelay(baseDelay, attempt) {
353
463
  return delay + jitter;
354
464
  }
355
465
 
356
- function retryDelayForError(error, baseDelay, attempt) {
357
- return parseRetryAfter(error.headers) ?? retryDelay(baseDelay, attempt);
358
- }
359
-
360
- function parseRetryAfter(headers) {
466
+ function parseRetryAfter(headers, cap = 30000) {
361
467
  const value = headerValue(headers, "retry-after");
362
468
  if (!value) return undefined;
363
469
  const seconds = Number(value);
364
470
  if (Number.isFinite(seconds) && seconds > 0) {
365
- return Math.min(seconds * 1000, 30000);
471
+ return Math.min(seconds * 1000, cap);
366
472
  }
367
473
  const date = Date.parse(value);
368
474
  if (Number.isFinite(date)) {
369
475
  const delay = date - Date.now();
370
- if (delay > 0) return Math.min(delay, 30000);
476
+ if (delay > 0) return Math.min(delay, cap);
371
477
  }
372
478
  return undefined;
373
479
  }
374
480
 
481
+ function asHookList(value) {
482
+ if (!value) return [];
483
+ return typeof value === "function" ? [value] : Array.from(value);
484
+ }
485
+
486
+ // Optional client-side throttle: caps concurrency and spaces requests to a
487
+ // maximum rate (requests per second).
488
+ class RateLimiter {
489
+ constructor(rps, concurrency) {
490
+ this.interval = rps > 0 ? 1000 / rps : 0;
491
+ this.limit = concurrency > 0 ? concurrency : Infinity;
492
+ this.active = 0;
493
+ this.waiters = [];
494
+ this.nextAllowed = 0;
495
+ }
496
+
497
+ async run(fn, signal) {
498
+ await this.#acquire();
499
+ try {
500
+ await this.#rateWait(signal);
501
+ return await fn();
502
+ } finally {
503
+ const next = this.waiters.shift();
504
+ if (next) {
505
+ next(); // transfer the slot to the next waiter (active unchanged)
506
+ } else {
507
+ this.active--;
508
+ }
509
+ }
510
+ }
511
+
512
+ #acquire() {
513
+ if (this.active < this.limit) {
514
+ this.active++;
515
+ return Promise.resolve();
516
+ }
517
+ // Wait for a slot to be transferred to us (active stays at the limit).
518
+ return new Promise((resolve) => this.waiters.push(resolve));
519
+ }
520
+
521
+ async #rateWait(signal) {
522
+ if (!this.interval) return;
523
+ const now = Date.now();
524
+ const wait = Math.max(0, this.nextAllowed - now);
525
+ this.nextAllowed = Math.max(now, this.nextAllowed) + this.interval;
526
+ if (wait > 0) await sleep(wait, signal);
527
+ }
528
+ }
529
+
530
+ function envVar(name) {
531
+ return (typeof process !== "undefined" && process.env && process.env[name]) || undefined;
532
+ }
533
+
534
+ function generateId() {
535
+ return (typeof crypto !== "undefined" && crypto.randomUUID) ? crypto.randomUUID() : `id-${Date.now()}-${Math.random().toString(16).slice(2)}`;
536
+ }
537
+
538
+ function ensureRequestId(headers) {
539
+ const existing = headerValue(headers, "x-request-id");
540
+ if (existing) return existing;
541
+ const id = generateId();
542
+ headers["x-request-id"] = id;
543
+ return id;
544
+ }
545
+
375
546
  function headerValue(headers, name) {
376
547
  for (const [key, value] of Object.entries(headers || {})) {
377
548
  if (key.toLowerCase() === name.toLowerCase()) return value;
package/src/index.d.ts CHANGED
@@ -8,6 +8,22 @@ import type {
8
8
 
9
9
  export type CrawloraParams = Record<string, unknown>;
10
10
 
11
+ export type CrawloraLogEvent = { event: string; [key: string]: unknown };
12
+
13
+ export interface CrawloraRequestContext {
14
+ operationId: string;
15
+ method: string;
16
+ url: string;
17
+ headers: Record<string, string>;
18
+ }
19
+ export type CrawloraBeforeRequest = (ctx: CrawloraRequestContext) => void | Promise<void>;
20
+ export type CrawloraAfterResponse = (
21
+ operationId: string,
22
+ status: number,
23
+ headers: Record<string, string>,
24
+ body: unknown
25
+ ) => unknown;
26
+
11
27
  export interface CrawloraClientOptions {
12
28
  apiKey?: string;
13
29
  jwtToken?: string;
@@ -15,6 +31,28 @@ export interface CrawloraClientOptions {
15
31
  timeout?: number;
16
32
  retries?: number;
17
33
  retryDelay?: number;
34
+ /** Cap on backoff and Retry-After delays in ms (default 30000). */
35
+ maxRetryDelay?: number;
36
+ /** Override the retryable HTTP status set (network status 0 stays retryable). */
37
+ retryStatuses?: Iterable<number>;
38
+ /** Full predicate; supersedes retryStatuses when set. */
39
+ isRetryable?: (status: number, error: CrawloraError) => boolean;
40
+ /** Called before each retry sleep with (attempt, error, delayMs). */
41
+ onRetry?: (attempt: number, error: CrawloraError, delay: number) => void;
42
+ /** Generate an x-request-id header when absent. */
43
+ requestId?: boolean;
44
+ /** Attach a stable Idempotency-Key on POST/PATCH, reused across retries. */
45
+ idempotencyKeys?: boolean;
46
+ /** Cap outgoing requests to at most this many per second. */
47
+ rateLimit?: number;
48
+ /** Cap the number of in-flight requests. */
49
+ maxConcurrency?: number;
50
+ /** Structured event sink (request/retry); never logs on its own. */
51
+ logger?: (event: CrawloraLogEvent) => void;
52
+ /** Hook(s) run before each request; mutate ctx.headers / ctx.url. */
53
+ beforeRequest?: CrawloraBeforeRequest | Iterable<CrawloraBeforeRequest>;
54
+ /** Hook(s) run on the parsed success body; return a value to replace it. */
55
+ afterResponse?: CrawloraAfterResponse | Iterable<CrawloraAfterResponse>;
18
56
  headers?: Record<string, string>;
19
57
  userAgent?: string | false;
20
58
  fetch?: typeof globalThis.fetch;
@@ -22,9 +60,13 @@ export interface CrawloraClientOptions {
22
60
 
23
61
  export interface CrawloraRequestOptions {
24
62
  headers?: Record<string, string>;
25
- responseType?: "auto" | "json" | "text";
63
+ responseType?: "auto" | "json" | "text" | "stream";
26
64
  timeout?: number;
27
65
  signal?: AbortSignal;
66
+ /** Per-request retry count, overriding the client default. */
67
+ retries?: number;
68
+ /** Per-request retry predicate, overriding the client default. */
69
+ isRetryable?: (status: number, error: CrawloraError) => boolean;
28
70
  }
29
71
 
30
72
  export type OperationMethod = <T = unknown>(
@@ -44,6 +86,8 @@ export interface OperationDefinition {
44
86
  consumes: string[];
45
87
  produces: string[];
46
88
  security: string[];
89
+ paginatable?: boolean;
90
+ cursorParams?: string[];
47
91
  }
48
92
 
49
93
  export class CrawloraError extends Error {
@@ -54,6 +98,7 @@ export class CrawloraError extends Error {
54
98
  response?: Response;
55
99
  cause?: unknown;
56
100
  retryable?: boolean;
101
+ requestId?: string;
57
102
  }
58
103
 
59
104
  // Thrown for 4xx API responses (request rejected by the API).
@@ -64,16 +109,25 @@ export class CrawloraServerError extends CrawloraError {}
64
109
  export class CrawloraNetworkError extends CrawloraError {}
65
110
 
66
111
  export interface CrawloraPaginateOptions extends CrawloraRequestOptions {
67
- // Query parameter to advance. Auto-detected as "page" or "offset" when omitted.
112
+ // Numeric query parameter to advance. Auto-detected as "page" or "offset".
68
113
  pageParam?: string;
69
- // First value for the page parameter. Defaults to 1 for "page", 0 for "offset".
70
- start?: number;
114
+ // Cursor/token query parameter (cursor mode). Requires nextCursor.
115
+ cursorParam?: string;
116
+ // Extracts the next cursor from a page; iteration stops when it returns falsy.
117
+ nextCursor?: (page: unknown) => unknown;
118
+ // First page value (numeric) or initial cursor value.
119
+ start?: unknown;
71
120
  // Amount added to the page parameter after each page. Defaults to 1.
72
121
  step?: number;
73
122
  // Maximum number of pages to fetch. Defaults to unbounded.
74
123
  maxPages?: number;
75
124
  }
76
125
 
126
+ export interface CrawloraPaginateItemsOptions extends CrawloraPaginateOptions {
127
+ // Extracts the item list from a page (default: the `data` array).
128
+ items?: (page: unknown) => Iterable<unknown>;
129
+ }
130
+
77
131
  export class CrawloraClient {
78
132
  constructor(options?: CrawloraClientOptions);
79
133
  request<I extends OperationId>(
@@ -89,6 +143,11 @@ export class CrawloraClient {
89
143
  params?: OperationParamsMap[I],
90
144
  options?: CrawloraPaginateOptions
91
145
  ): AsyncGenerator<OperationResponseMap[I], void, unknown>;
146
+ paginateItems<I extends OperationId>(
147
+ operationId: I,
148
+ params?: OperationParamsMap[I],
149
+ options?: CrawloraPaginateItemsOptions
150
+ ): AsyncGenerator<unknown, void, unknown>;
92
151
  [group: string]: unknown;
93
152
  }
94
153
 
package/src/operations.js CHANGED
@@ -51,6 +51,7 @@ export const operations = {
51
51
  "formParams": [],
52
52
  "id": "airbnb-room-reviews",
53
53
  "method": "GET",
54
+ "paginatable": true,
54
55
  "path": "/airbnb/room/{id}/reviews",
55
56
  "pathParams": [
56
57
  "id"
@@ -78,6 +79,7 @@ export const operations = {
78
79
  "formParams": [],
79
80
  "id": "airbnb-search",
80
81
  "method": "GET",
82
+ "paginatable": true,
81
83
  "path": "/airbnb/search",
82
84
  "pathParams": [],
83
85
  "produces": [
@@ -192,6 +194,7 @@ export const operations = {
192
194
  "formParams": [],
193
195
  "id": "amazon-search",
194
196
  "method": "GET",
197
+ "paginatable": true,
195
198
  "path": "/amazon/search",
196
199
  "pathParams": [],
197
200
  "produces": [
@@ -289,6 +292,7 @@ export const operations = {
289
292
  "formParams": [],
290
293
  "id": "apple-podcasts-episodes-search",
291
294
  "method": "GET",
295
+ "paginatable": true,
292
296
  "path": "/apple-podcasts/episodes/search",
293
297
  "pathParams": [],
294
298
  "produces": [
@@ -335,6 +339,7 @@ export const operations = {
335
339
  "formParams": [],
336
340
  "id": "apple-podcasts-search",
337
341
  "method": "GET",
342
+ "paginatable": true,
338
343
  "path": "/apple-podcasts/search",
339
344
  "pathParams": [],
340
345
  "produces": [
@@ -649,6 +654,7 @@ export const operations = {
649
654
  "formParams": [],
650
655
  "id": "appstore-reviews",
651
656
  "method": "GET",
657
+ "paginatable": true,
652
658
  "path": "/appstore/reviews",
653
659
  "pathParams": [],
654
660
  "produces": [
@@ -703,6 +709,7 @@ export const operations = {
703
709
  "formParams": [],
704
710
  "id": "appstore-search",
705
711
  "method": "GET",
712
+ "paginatable": true,
706
713
  "path": "/appstore/search",
707
714
  "pathParams": [],
708
715
  "produces": [
@@ -1070,6 +1077,7 @@ export const operations = {
1070
1077
  "formParams": [],
1071
1078
  "id": "bing-images",
1072
1079
  "method": "GET",
1080
+ "paginatable": true,
1073
1081
  "path": "/bing/images",
1074
1082
  "pathParams": [],
1075
1083
  "produces": [
@@ -1116,6 +1124,7 @@ export const operations = {
1116
1124
  "formParams": [],
1117
1125
  "id": "bing-news",
1118
1126
  "method": "GET",
1127
+ "paginatable": true,
1119
1128
  "path": "/bing/news",
1120
1129
  "pathParams": [],
1121
1130
  "produces": [
@@ -1162,6 +1171,7 @@ export const operations = {
1162
1171
  "formParams": [],
1163
1172
  "id": "bing-search",
1164
1173
  "method": "GET",
1174
+ "paginatable": true,
1165
1175
  "path": "/bing/search",
1166
1176
  "pathParams": [],
1167
1177
  "produces": [
@@ -1249,6 +1259,7 @@ export const operations = {
1249
1259
  "formParams": [],
1250
1260
  "id": "bing-videos",
1251
1261
  "method": "GET",
1262
+ "paginatable": true,
1252
1263
  "path": "/bing/videos",
1253
1264
  "pathParams": [],
1254
1265
  "produces": [
@@ -1295,6 +1306,7 @@ export const operations = {
1295
1306
  "formParams": [],
1296
1307
  "id": "brave-images",
1297
1308
  "method": "GET",
1309
+ "paginatable": true,
1298
1310
  "path": "/brave/images",
1299
1311
  "pathParams": [],
1300
1312
  "produces": [
@@ -1397,6 +1409,7 @@ export const operations = {
1397
1409
  "formParams": [],
1398
1410
  "id": "brave-news",
1399
1411
  "method": "GET",
1412
+ "paginatable": true,
1400
1413
  "path": "/brave/news",
1401
1414
  "pathParams": [],
1402
1415
  "produces": [
@@ -1522,6 +1535,7 @@ export const operations = {
1522
1535
  "formParams": [],
1523
1536
  "id": "brave-search",
1524
1537
  "method": "GET",
1538
+ "paginatable": true,
1525
1539
  "path": "/brave/search",
1526
1540
  "pathParams": [],
1527
1541
  "produces": [
@@ -1739,6 +1753,7 @@ export const operations = {
1739
1753
  "formParams": [],
1740
1754
  "id": "brave-videos",
1741
1755
  "method": "GET",
1756
+ "paginatable": true,
1742
1757
  "path": "/brave/videos",
1743
1758
  "pathParams": [],
1744
1759
  "produces": [
@@ -1959,6 +1974,7 @@ export const operations = {
1959
1974
  "formParams": [],
1960
1975
  "id": "coingecko-category-coins",
1961
1976
  "method": "GET",
1977
+ "paginatable": true,
1962
1978
  "path": "/coingecko/category/{slug}/coins",
1963
1979
  "pathParams": [
1964
1980
  "slug"
@@ -2548,6 +2564,7 @@ export const operations = {
2548
2564
  "formParams": [],
2549
2565
  "id": "coingecko-exchanges",
2550
2566
  "method": "GET",
2567
+ "paginatable": true,
2551
2568
  "path": "/coingecko/exchanges",
2552
2569
  "pathParams": [],
2553
2570
  "produces": [
@@ -2863,6 +2880,7 @@ export const operations = {
2863
2880
  "formParams": [],
2864
2881
  "id": "coingecko-markets",
2865
2882
  "method": "GET",
2883
+ "paginatable": true,
2866
2884
  "path": "/coingecko/markets",
2867
2885
  "pathParams": [],
2868
2886
  "produces": [
@@ -2963,6 +2981,7 @@ export const operations = {
2963
2981
  "formParams": [],
2964
2982
  "id": "coingecko-new-coins",
2965
2983
  "method": "GET",
2984
+ "paginatable": true,
2966
2985
  "path": "/coingecko/new-coins",
2967
2986
  "pathParams": [],
2968
2987
  "produces": [
@@ -3088,6 +3107,7 @@ export const operations = {
3088
3107
  "formParams": [],
3089
3108
  "id": "coingecko-nft-category",
3090
3109
  "method": "GET",
3110
+ "paginatable": true,
3091
3111
  "path": "/coingecko/nft/category/{slug}",
3092
3112
  "pathParams": [
3093
3113
  "slug"
@@ -3190,6 +3210,7 @@ export const operations = {
3190
3210
  "formParams": [],
3191
3211
  "id": "coingecko-nfts",
3192
3212
  "method": "GET",
3213
+ "paginatable": true,
3193
3214
  "path": "/coingecko/nfts",
3194
3215
  "pathParams": [],
3195
3216
  "produces": [
@@ -3682,6 +3703,7 @@ export const operations = {
3682
3703
  "formParams": [],
3683
3704
  "id": "datasets-google-map-businesses-nearby",
3684
3705
  "method": "GET",
3706
+ "paginatable": true,
3685
3707
  "path": "/datasets/google-map-businesses/nearby",
3686
3708
  "pathParams": [],
3687
3709
  "produces": [
@@ -3745,6 +3767,7 @@ export const operations = {
3745
3767
  "formParams": [],
3746
3768
  "id": "datasets-google-map-businesses-search",
3747
3769
  "method": "GET",
3770
+ "paginatable": true,
3748
3771
  "path": "/datasets/google-map-businesses/search",
3749
3772
  "pathParams": [],
3750
3773
  "produces": [
@@ -3951,6 +3974,7 @@ export const operations = {
3951
3974
  "formParams": [],
3952
3975
  "id": "ebay-seller-feedback",
3953
3976
  "method": "GET",
3977
+ "paginatable": true,
3954
3978
  "path": "/ebay/seller/{seller}/feedback",
3955
3979
  "pathParams": [
3956
3980
  "seller"
@@ -3988,6 +4012,7 @@ export const operations = {
3988
4012
  "formParams": [],
3989
4013
  "id": "ebay-seller-shop",
3990
4014
  "method": "GET",
4015
+ "paginatable": true,
3991
4016
  "path": "/ebay/seller/{seller}/shop",
3992
4017
  "pathParams": [
3993
4018
  "seller"
@@ -4340,6 +4365,7 @@ export const operations = {
4340
4365
  "formParams": [],
4341
4366
  "id": "google-finance-markets-category-news",
4342
4367
  "method": "GET",
4368
+ "paginatable": true,
4343
4369
  "path": "/google/finance/markets/categories/{category}/news",
4344
4370
  "pathParams": [
4345
4371
  "category"
@@ -4367,6 +4393,7 @@ export const operations = {
4367
4393
  "formParams": [],
4368
4394
  "id": "google-finance-markets-category-stocks",
4369
4395
  "method": "GET",
4396
+ "paginatable": true,
4370
4397
  "path": "/google/finance/markets/categories/{category}/stocks",
4371
4398
  "pathParams": [
4372
4399
  "category"
@@ -4470,6 +4497,7 @@ export const operations = {
4470
4497
  "formParams": [],
4471
4498
  "id": "google-finance-markets-movers",
4472
4499
  "method": "GET",
4500
+ "paginatable": true,
4473
4501
  "path": "/google/finance/markets/movers",
4474
4502
  "pathParams": [],
4475
4503
  "produces": [
@@ -4505,6 +4533,7 @@ export const operations = {
4505
4533
  "formParams": [],
4506
4534
  "id": "google-finance-markets-top",
4507
4535
  "method": "GET",
4536
+ "paginatable": true,
4508
4537
  "path": "/google/finance/markets/top",
4509
4538
  "pathParams": [],
4510
4539
  "produces": [
@@ -6773,9 +6802,13 @@ export const operations = {
6773
6802
  "consumes": [
6774
6803
  "application/json"
6775
6804
  ],
6805
+ "cursorParams": [
6806
+ "cursor"
6807
+ ],
6776
6808
  "formParams": [],
6777
6809
  "id": "producthunt-alternatives",
6778
6810
  "method": "GET",
6811
+ "paginatable": true,
6779
6812
  "path": "/producthunt/product/{id}/alternatives",
6780
6813
  "pathParams": [
6781
6814
  "id"
@@ -6839,6 +6872,7 @@ export const operations = {
6839
6872
  "formParams": [],
6840
6873
  "id": "producthunt-category-products",
6841
6874
  "method": "GET",
6875
+ "paginatable": true,
6842
6876
  "path": "/producthunt/category/{slug}/products",
6843
6877
  "pathParams": [
6844
6878
  "slug"
@@ -6886,6 +6920,7 @@ export const operations = {
6886
6920
  "formParams": [],
6887
6921
  "id": "producthunt-customers",
6888
6922
  "method": "GET",
6923
+ "paginatable": true,
6889
6924
  "path": "/producthunt/product/{id}/customers",
6890
6925
  "pathParams": [
6891
6926
  "id"
@@ -6924,9 +6959,13 @@ export const operations = {
6924
6959
  "consumes": [
6925
6960
  "application/json"
6926
6961
  ],
6962
+ "cursorParams": [
6963
+ "cursor"
6964
+ ],
6927
6965
  "formParams": [],
6928
6966
  "id": "producthunt-launches",
6929
6967
  "method": "GET",
6968
+ "paginatable": true,
6930
6969
  "path": "/producthunt/product/{id}/launches",
6931
6970
  "pathParams": [
6932
6971
  "id"
@@ -6956,9 +6995,13 @@ export const operations = {
6956
6995
  "consumes": [
6957
6996
  "application/json"
6958
6997
  ],
6998
+ "cursorParams": [
6999
+ "cursor"
7000
+ ],
6959
7001
  "formParams": [],
6960
7002
  "id": "producthunt-leaderboard",
6961
7003
  "method": "GET",
7004
+ "paginatable": true,
6962
7005
  "path": "/producthunt/leaderboard",
6963
7006
  "pathParams": [],
6964
7007
  "produces": [
@@ -7027,9 +7070,13 @@ export const operations = {
7027
7070
  "consumes": [
7028
7071
  "application/json"
7029
7072
  ],
7073
+ "cursorParams": [
7074
+ "cursor"
7075
+ ],
7030
7076
  "formParams": [],
7031
7077
  "id": "producthunt-makers",
7032
7078
  "method": "GET",
7079
+ "paginatable": true,
7033
7080
  "path": "/producthunt/product/{id}/makers",
7034
7081
  "pathParams": [
7035
7082
  "id"
@@ -7099,6 +7146,7 @@ export const operations = {
7099
7146
  "formParams": [],
7100
7147
  "id": "producthunt-search",
7101
7148
  "method": "GET",
7149
+ "paginatable": true,
7102
7150
  "path": "/producthunt/search",
7103
7151
  "pathParams": [],
7104
7152
  "produces": [
@@ -7726,6 +7774,7 @@ export const operations = {
7726
7774
  "formParams": [],
7727
7775
  "id": "shopify-collection-products",
7728
7776
  "method": "GET",
7777
+ "paginatable": true,
7729
7778
  "path": "/shopify/collections/{handle}/products",
7730
7779
  "pathParams": [
7731
7780
  "handle"
@@ -7764,6 +7813,7 @@ export const operations = {
7764
7813
  "formParams": [],
7765
7814
  "id": "shopify-collections",
7766
7815
  "method": "GET",
7816
+ "paginatable": true,
7767
7817
  "path": "/shopify/collections",
7768
7818
  "pathParams": [],
7769
7819
  "produces": [
@@ -7828,6 +7878,7 @@ export const operations = {
7828
7878
  "formParams": [],
7829
7879
  "id": "shopify-pages",
7830
7880
  "method": "GET",
7881
+ "paginatable": true,
7831
7882
  "path": "/shopify/pages",
7832
7883
  "pathParams": [],
7833
7884
  "produces": [
@@ -7934,6 +7985,7 @@ export const operations = {
7934
7985
  "formParams": [],
7935
7986
  "id": "shopify-products",
7936
7987
  "method": "GET",
7988
+ "paginatable": true,
7937
7989
  "path": "/shopify/products",
7938
7990
  "pathParams": [],
7939
7991
  "produces": [
@@ -8156,6 +8208,7 @@ export const operations = {
8156
8208
  "formParams": [],
8157
8209
  "id": "spotify-album",
8158
8210
  "method": "GET",
8211
+ "paginatable": true,
8159
8212
  "path": "/spotify/album",
8160
8213
  "pathParams": [],
8161
8214
  "produces": [
@@ -8196,6 +8249,7 @@ export const operations = {
8196
8249
  "formParams": [],
8197
8250
  "id": "spotify-album-tracks",
8198
8251
  "method": "GET",
8252
+ "paginatable": true,
8199
8253
  "path": "/spotify/album/tracks",
8200
8254
  "pathParams": [],
8201
8255
  "produces": [
@@ -8236,6 +8290,7 @@ export const operations = {
8236
8290
  "formParams": [],
8237
8291
  "id": "spotify-albums-search",
8238
8292
  "method": "GET",
8293
+ "paginatable": true,
8239
8294
  "path": "/spotify/albums/search",
8240
8295
  "pathParams": [],
8241
8296
  "produces": [
@@ -8332,6 +8387,7 @@ export const operations = {
8332
8387
  "formParams": [],
8333
8388
  "id": "spotify-artist-albums",
8334
8389
  "method": "GET",
8390
+ "paginatable": true,
8335
8391
  "path": "/spotify/artist/albums",
8336
8392
  "pathParams": [],
8337
8393
  "produces": [
@@ -8442,6 +8498,7 @@ export const operations = {
8442
8498
  "formParams": [],
8443
8499
  "id": "spotify-artists-search",
8444
8500
  "method": "GET",
8501
+ "paginatable": true,
8445
8502
  "path": "/spotify/artists/search",
8446
8503
  "pathParams": [],
8447
8504
  "produces": [
@@ -8508,6 +8565,7 @@ export const operations = {
8508
8565
  "formParams": [],
8509
8566
  "id": "spotify-audiobook-chapters",
8510
8567
  "method": "GET",
8568
+ "paginatable": true,
8511
8569
  "path": "/spotify/audiobook/chapters",
8512
8570
  "pathParams": [],
8513
8571
  "produces": [
@@ -8548,6 +8606,7 @@ export const operations = {
8548
8606
  "formParams": [],
8549
8607
  "id": "spotify-audiobooks-search",
8550
8608
  "method": "GET",
8609
+ "paginatable": true,
8551
8610
  "path": "/spotify/audiobooks/search",
8552
8611
  "pathParams": [],
8553
8612
  "produces": [
@@ -8644,6 +8703,7 @@ export const operations = {
8644
8703
  "formParams": [],
8645
8704
  "id": "spotify-episodes-search",
8646
8705
  "method": "GET",
8706
+ "paginatable": true,
8647
8707
  "path": "/spotify/episodes/search",
8648
8708
  "pathParams": [],
8649
8709
  "produces": [
@@ -8805,6 +8865,7 @@ export const operations = {
8805
8865
  "formParams": [],
8806
8866
  "id": "spotify-playlist",
8807
8867
  "method": "GET",
8868
+ "paginatable": true,
8808
8869
  "path": "/spotify/playlist",
8809
8870
  "pathParams": [],
8810
8871
  "produces": [
@@ -8855,6 +8916,7 @@ export const operations = {
8855
8916
  "formParams": [],
8856
8917
  "id": "spotify-playlists-search",
8857
8918
  "method": "GET",
8919
+ "paginatable": true,
8858
8920
  "path": "/spotify/playlists/search",
8859
8921
  "pathParams": [],
8860
8922
  "produces": [
@@ -9086,6 +9148,7 @@ export const operations = {
9086
9148
  "formParams": [],
9087
9149
  "id": "spotify-podcasts-search",
9088
9150
  "method": "GET",
9151
+ "paginatable": true,
9089
9152
  "path": "/spotify-podcasts/search",
9090
9153
  "pathParams": [],
9091
9154
  "produces": [
@@ -9187,6 +9250,7 @@ export const operations = {
9187
9250
  "formParams": [],
9188
9251
  "id": "spotify-podcasts-show-episodes",
9189
9252
  "method": "GET",
9253
+ "paginatable": true,
9190
9254
  "path": "/spotify-podcasts/show/episodes",
9191
9255
  "pathParams": [],
9192
9256
  "produces": [
@@ -9327,6 +9391,7 @@ export const operations = {
9327
9391
  "formParams": [],
9328
9392
  "id": "spotify-profile-followers",
9329
9393
  "method": "GET",
9394
+ "paginatable": true,
9330
9395
  "path": "/spotify/profile/followers",
9331
9396
  "pathParams": [],
9332
9397
  "produces": [
@@ -9372,6 +9437,7 @@ export const operations = {
9372
9437
  "formParams": [],
9373
9438
  "id": "spotify-profile-playlists",
9374
9439
  "method": "GET",
9440
+ "paginatable": true,
9375
9441
  "path": "/spotify/profile/playlists",
9376
9442
  "pathParams": [],
9377
9443
  "produces": [
@@ -9417,6 +9483,7 @@ export const operations = {
9417
9483
  "formParams": [],
9418
9484
  "id": "spotify-profiles-search",
9419
9485
  "method": "GET",
9486
+ "paginatable": true,
9420
9487
  "path": "/spotify/profiles/search",
9421
9488
  "pathParams": [],
9422
9489
  "produces": [
@@ -9483,6 +9550,7 @@ export const operations = {
9483
9550
  "formParams": [],
9484
9551
  "id": "spotify-search",
9485
9552
  "method": "GET",
9553
+ "paginatable": true,
9486
9554
  "path": "/spotify/search",
9487
9555
  "pathParams": [],
9488
9556
  "produces": [
@@ -9559,6 +9627,7 @@ export const operations = {
9559
9627
  "formParams": [],
9560
9628
  "id": "spotify-section",
9561
9629
  "method": "GET",
9630
+ "paginatable": true,
9562
9631
  "path": "/spotify/section",
9563
9632
  "pathParams": [],
9564
9633
  "produces": [
@@ -9599,6 +9668,7 @@ export const operations = {
9599
9668
  "formParams": [],
9600
9669
  "id": "spotify-shows-search",
9601
9670
  "method": "GET",
9671
+ "paginatable": true,
9602
9672
  "path": "/spotify/shows/search",
9603
9673
  "pathParams": [],
9604
9674
  "produces": [
@@ -9740,6 +9810,7 @@ export const operations = {
9740
9810
  "formParams": [],
9741
9811
  "id": "spotify-tracks-search",
9742
9812
  "method": "GET",
9813
+ "paginatable": true,
9743
9814
  "path": "/spotify/tracks/search",
9744
9815
  "pathParams": [],
9745
9816
  "produces": [
@@ -9843,9 +9914,13 @@ export const operations = {
9843
9914
  "consumes": [
9844
9915
  "application/json"
9845
9916
  ],
9917
+ "cursorParams": [
9918
+ "cursor"
9919
+ ],
9846
9920
  "formParams": [],
9847
9921
  "id": "tiktok-challenge-list",
9848
9922
  "method": "GET",
9923
+ "paginatable": true,
9849
9924
  "path": "/tiktok/hashtags",
9850
9925
  "pathParams": [],
9851
9926
  "produces": [
@@ -9917,6 +9992,7 @@ export const operations = {
9917
9992
  "formParams": [],
9918
9993
  "id": "tiktok-popular-trend-creator",
9919
9994
  "method": "GET",
9995
+ "paginatable": true,
9920
9996
  "path": "/tiktok/popular-trend/creator",
9921
9997
  "pathParams": [],
9922
9998
  "produces": [
@@ -10012,9 +10088,13 @@ export const operations = {
10012
10088
  "consumes": [
10013
10089
  "application/json"
10014
10090
  ],
10091
+ "cursorParams": [
10092
+ "cursor"
10093
+ ],
10015
10094
  "formParams": [],
10016
10095
  "id": "tiktok-profile-post",
10017
10096
  "method": "GET",
10097
+ "paginatable": true,
10018
10098
  "path": "/tiktok/posts",
10019
10099
  "pathParams": [],
10020
10100
  "produces": [
@@ -10053,9 +10133,13 @@ export const operations = {
10053
10133
  "consumes": [
10054
10134
  "application/json"
10055
10135
  ],
10136
+ "cursorParams": [
10137
+ "cursor"
10138
+ ],
10056
10139
  "formParams": [],
10057
10140
  "id": "tiktok-search",
10058
10141
  "method": "GET",
10142
+ "paginatable": true,
10059
10143
  "path": "/tiktok/search",
10060
10144
  "pathParams": [],
10061
10145
  "produces": [
@@ -10089,9 +10173,13 @@ export const operations = {
10089
10173
  "consumes": [
10090
10174
  "application/json"
10091
10175
  ],
10176
+ "cursorParams": [
10177
+ "cursor"
10178
+ ],
10092
10179
  "formParams": [],
10093
10180
  "id": "tiktok-search-hashtag",
10094
10181
  "method": "GET",
10182
+ "paginatable": true,
10095
10183
  "path": "/tiktok/search/hashtag",
10096
10184
  "pathParams": [],
10097
10185
  "produces": [
@@ -10125,9 +10213,13 @@ export const operations = {
10125
10213
  "consumes": [
10126
10214
  "application/json"
10127
10215
  ],
10216
+ "cursorParams": [
10217
+ "cursor"
10218
+ ],
10128
10219
  "formParams": [],
10129
10220
  "id": "tiktok-search-user",
10130
10221
  "method": "GET",
10222
+ "paginatable": true,
10131
10223
  "path": "/tiktok/search/user",
10132
10224
  "pathParams": [],
10133
10225
  "produces": [
@@ -10252,6 +10344,7 @@ export const operations = {
10252
10344
  "formParams": [],
10253
10345
  "id": "tiktok-top-ads-list",
10254
10346
  "method": "GET",
10347
+ "paginatable": true,
10255
10348
  "path": "/tiktok/top-ads/list",
10256
10349
  "pathParams": [],
10257
10350
  "produces": [
@@ -10414,6 +10507,7 @@ export const operations = {
10414
10507
  "formParams": [],
10415
10508
  "id": "tiktok-top-ads-recommend",
10416
10509
  "method": "GET",
10510
+ "paginatable": true,
10417
10511
  "path": "/tiktok/top-ads/recommend",
10418
10512
  "pathParams": [],
10419
10513
  "produces": [
@@ -10469,6 +10563,7 @@ export const operations = {
10469
10563
  "formParams": [],
10470
10564
  "id": "tiktok-top-ads-spotlight",
10471
10565
  "method": "GET",
10566
+ "paginatable": true,
10472
10567
  "path": "/tiktok/top-ads/spotlight",
10473
10568
  "pathParams": [],
10474
10569
  "produces": [
@@ -10545,9 +10640,13 @@ export const operations = {
10545
10640
  "consumes": [
10546
10641
  "application/json"
10547
10642
  ],
10643
+ "cursorParams": [
10644
+ "cursor"
10645
+ ],
10548
10646
  "formParams": [],
10549
10647
  "id": "tiktok-video-comments",
10550
10648
  "method": "GET",
10649
+ "paginatable": true,
10551
10650
  "path": "/tiktok/comments",
10552
10651
  "pathParams": [],
10553
10652
  "produces": [
@@ -10657,6 +10756,7 @@ export const operations = {
10657
10756
  "formParams": [],
10658
10757
  "id": "tripadvisor-hotels",
10659
10758
  "method": "GET",
10759
+ "paginatable": true,
10660
10760
  "path": "/tripadvisor/hotels",
10661
10761
  "pathParams": [],
10662
10762
  "produces": [
@@ -10774,6 +10874,7 @@ export const operations = {
10774
10874
  "formParams": [],
10775
10875
  "id": "tripadvisor-reviews",
10776
10876
  "method": "GET",
10877
+ "paginatable": true,
10777
10878
  "path": "/tripadvisor/reviews",
10778
10879
  "pathParams": [],
10779
10880
  "produces": [
@@ -10845,6 +10946,7 @@ export const operations = {
10845
10946
  "formParams": [],
10846
10947
  "id": "tripadvisor-search",
10847
10948
  "method": "GET",
10949
+ "paginatable": true,
10848
10950
  "path": "/tripadvisor/search",
10849
10951
  "pathParams": [],
10850
10952
  "produces": [
@@ -11012,6 +11114,7 @@ export const operations = {
11012
11114
  "formParams": [],
11013
11115
  "id": "trustpilot-business-reviews",
11014
11116
  "method": "GET",
11117
+ "paginatable": true,
11015
11118
  "path": "/trustpilot/business/{slug}/reviews",
11016
11119
  "pathParams": [
11017
11120
  "slug"
@@ -11074,6 +11177,7 @@ export const operations = {
11074
11177
  "formParams": [],
11075
11178
  "id": "trustpilot-business-search",
11076
11179
  "method": "GET",
11180
+ "paginatable": true,
11077
11181
  "path": "/trustpilot/business-units/search",
11078
11182
  "pathParams": [],
11079
11183
  "produces": [
@@ -11134,6 +11238,7 @@ export const operations = {
11134
11238
  "formParams": [],
11135
11239
  "id": "trustpilot-category",
11136
11240
  "method": "GET",
11241
+ "paginatable": true,
11137
11242
  "path": "/trustpilot/category/{slug}",
11138
11243
  "pathParams": [
11139
11244
  "slug"
@@ -11469,9 +11574,13 @@ export const operations = {
11469
11574
  "consumes": [
11470
11575
  "application/json"
11471
11576
  ],
11577
+ "cursorParams": [
11578
+ "start"
11579
+ ],
11472
11580
  "formParams": [],
11473
11581
  "id": "yahoo-finance-calendar",
11474
11582
  "method": "GET",
11583
+ "paginatable": true,
11475
11584
  "path": "/yahoo-finance/calendars/{type}",
11476
11585
  "pathParams": [
11477
11586
  "type"
@@ -11644,6 +11753,7 @@ export const operations = {
11644
11753
  "formParams": [],
11645
11754
  "id": "yahoo-finance-screener",
11646
11755
  "method": "GET",
11756
+ "paginatable": true,
11647
11757
  "path": "/yahoo-finance/screener/{id}",
11648
11758
  "pathParams": [
11649
11759
  "id"
@@ -11941,6 +12051,7 @@ export const operations = {
11941
12051
  "formParams": [],
11942
12052
  "id": "yahoo-finance-ticker-earnings-dates",
11943
12053
  "method": "GET",
12054
+ "paginatable": true,
11944
12055
  "path": "/yahoo-finance/ticker/{symbol}/earnings-dates",
11945
12056
  "pathParams": [
11946
12057
  "symbol"
@@ -12023,9 +12134,13 @@ export const operations = {
12023
12134
  "consumes": [
12024
12135
  "application/json"
12025
12136
  ],
12137
+ "cursorParams": [
12138
+ "start"
12139
+ ],
12026
12140
  "formParams": [],
12027
12141
  "id": "yahoo-finance-ticker-history",
12028
12142
  "method": "GET",
12143
+ "paginatable": true,
12029
12144
  "path": "/yahoo-finance/ticker/{symbol}/history",
12030
12145
  "pathParams": [
12031
12146
  "symbol"
@@ -12317,9 +12432,13 @@ export const operations = {
12317
12432
  "consumes": [
12318
12433
  "application/json"
12319
12434
  ],
12435
+ "cursorParams": [
12436
+ "start"
12437
+ ],
12320
12438
  "formParams": [],
12321
12439
  "id": "yahoo-finance-ticker-shares-full",
12322
12440
  "method": "GET",
12441
+ "paginatable": true,
12323
12442
  "path": "/yahoo-finance/ticker/{symbol}/shares-full",
12324
12443
  "pathParams": [
12325
12444
  "symbol"
@@ -12925,6 +13044,7 @@ export const operations = {
12925
13044
  "formParams": [],
12926
13045
  "id": "zillow-search",
12927
13046
  "method": "GET",
13047
+ "paginatable": true,
12928
13048
  "path": "/zillow/search",
12929
13049
  "pathParams": [],
12930
13050
  "produces": [