@datafuel/sdk 0.1.0 → 0.3.0
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/README.md +53 -10
- package/dist/index.cjs +284 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -8
- package/dist/index.d.ts +321 -8
- package/dist/index.js +282 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +215 -17
- package/src/core.ts +172 -4
- package/src/errors.ts +47 -2
- package/src/index.ts +30 -1
- package/src/models.ts +247 -1
package/src/client.ts
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
/** The client. It only moves bytes; `core` decides what goes on the wire. */
|
|
2
2
|
|
|
3
3
|
import * as core from "./core.js";
|
|
4
|
-
import type { AskOptions, CrawlOptions, MapOptions } from "./core.js";
|
|
4
|
+
import type { AskOptions, CrawlOptions, MapOptions, SearchOptions } from "./core.js";
|
|
5
5
|
import { apiError, DataFuelError, NoApiKey, TransportError, WaitTimeout } from "./errors.js";
|
|
6
6
|
import type {
|
|
7
|
+
Analytics,
|
|
8
|
+
AnalyticsOptions,
|
|
9
|
+
BalanceSplit,
|
|
7
10
|
CallOptions,
|
|
11
|
+
CancelResult,
|
|
8
12
|
Capability,
|
|
9
13
|
CrawlResult,
|
|
10
14
|
CrawlResultsPage,
|
|
11
15
|
CrawlStatus,
|
|
12
16
|
JobResults,
|
|
17
|
+
JobsPage,
|
|
13
18
|
JobStatus,
|
|
19
|
+
JobSummary,
|
|
20
|
+
JsInstruction,
|
|
21
|
+
ListOptions,
|
|
22
|
+
ListTasksOptions,
|
|
14
23
|
Profile,
|
|
24
|
+
ProxyCountry,
|
|
25
|
+
ProxyLocation,
|
|
15
26
|
ScrapeOptions,
|
|
16
27
|
SiteMap,
|
|
28
|
+
TasksPage,
|
|
29
|
+
TaskSummary,
|
|
30
|
+
TransactionsOptions,
|
|
31
|
+
TransactionsPage,
|
|
17
32
|
} from "./models.js";
|
|
18
33
|
import { Capabilities, CrawlPage, isDone, Result } from "./models.js";
|
|
19
34
|
|
|
@@ -46,7 +61,8 @@ type Send = CallOptions & { timeoutMs?: number; signal?: AbortSignal };
|
|
|
46
61
|
*
|
|
47
62
|
* Pick the call by the shape of the work: one URL is {@link scrape}, a site's
|
|
48
63
|
* URL list is {@link map}, many pages from a start URL is {@link crawl}, a list
|
|
49
|
-
* of known URLs is {@link runJob}, a question for an AI engine is {@link ask}
|
|
64
|
+
* of known URLs is {@link runJob}, a question for an AI engine is {@link ask}, a
|
|
65
|
+
* Google search is {@link search}.
|
|
50
66
|
*
|
|
51
67
|
* Every write carries an `Idempotency-Key`, generated per request, so a retry
|
|
52
68
|
* attaches to the task already running instead of charging twice.
|
|
@@ -84,7 +100,7 @@ export class DataFuel {
|
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
private async send(request: core.Request, opts: Send = {}): Promise<unknown> {
|
|
87
|
-
if (!this.apiKey) {
|
|
103
|
+
if (!this.apiKey && request.auth) {
|
|
88
104
|
throw new NoApiKey("no API key: pass one to the client or set DATAFUEL_API_KEY");
|
|
89
105
|
}
|
|
90
106
|
const url = new URL(this.baseUrl + request.path);
|
|
@@ -205,6 +221,12 @@ export class DataFuel {
|
|
|
205
221
|
return this.runTask(request, options);
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/** Run a Google search and return the results page, parsed to JSON by default. */
|
|
225
|
+
async search(query: string, options: SearchOptions & CallOptions = {}): Promise<Result> {
|
|
226
|
+
const request = core.buildSearch(query, options, core.key(options.idempotencyKey));
|
|
227
|
+
return this.runTask(request, options);
|
|
228
|
+
}
|
|
229
|
+
|
|
208
230
|
/**
|
|
209
231
|
* List the URLs of a site without scraping them.
|
|
210
232
|
*
|
|
@@ -232,6 +254,19 @@ export class DataFuel {
|
|
|
232
254
|
return strField(await this.send(request, options), "job_id");
|
|
233
255
|
}
|
|
234
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Stop a crawl. Queued pages are refunded, pages in flight finish and bill.
|
|
259
|
+
* Throws {@link JobNotCancellable} when it already finished.
|
|
260
|
+
*/
|
|
261
|
+
async cancelCrawl(crawlId: string, options: CallOptions = {}): Promise<CancelResult> {
|
|
262
|
+
return cancelResult(
|
|
263
|
+
await this.send(
|
|
264
|
+
new core.Request("POST", `/crawl/${core.pathSegment(crawlId)}/cancel`),
|
|
265
|
+
options,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
235
270
|
/** Return the progress of a crawl. */
|
|
236
271
|
async getCrawl(crawlId: string, options: CallOptions = {}): Promise<CrawlStatus> {
|
|
237
272
|
const body = record(
|
|
@@ -363,20 +398,35 @@ export class DataFuel {
|
|
|
363
398
|
return strField(await this.send(request, options), "id");
|
|
364
399
|
}
|
|
365
400
|
|
|
401
|
+
/** Queue a batch of Google searches and return the job id. */
|
|
402
|
+
async createSearchJob(
|
|
403
|
+
queries: string[],
|
|
404
|
+
options: SearchOptions & CallOptions & { sequential?: boolean } = {},
|
|
405
|
+
): Promise<string> {
|
|
406
|
+
const request = core.buildSearchJob(
|
|
407
|
+
queries,
|
|
408
|
+
options,
|
|
409
|
+
options.sequential ?? false,
|
|
410
|
+
core.key(options.idempotencyKey),
|
|
411
|
+
);
|
|
412
|
+
return strField(await this.send(request, options), "id");
|
|
413
|
+
}
|
|
414
|
+
|
|
366
415
|
/** Return the progress of a job. */
|
|
367
416
|
async getJob(jobId: string, options: CallOptions = {}): Promise<JobStatus> {
|
|
368
|
-
|
|
417
|
+
return jobStatus(
|
|
369
418
|
await this.send(new core.Request("GET", `/job/${core.pathSegment(jobId)}`), options),
|
|
370
419
|
);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Stop a job. Queued tasks are refunded, tasks in flight finish and bill.
|
|
424
|
+
* Throws {@link JobNotCancellable} when it already finished.
|
|
425
|
+
*/
|
|
426
|
+
async cancelJob(jobId: string, options: CallOptions = {}): Promise<CancelResult> {
|
|
427
|
+
return cancelResult(
|
|
428
|
+
await this.send(new core.Request("POST", `/job/${core.pathSegment(jobId)}/cancel`), options),
|
|
429
|
+
);
|
|
380
430
|
}
|
|
381
431
|
|
|
382
432
|
/**
|
|
@@ -399,6 +449,35 @@ export class DataFuel {
|
|
|
399
449
|
};
|
|
400
450
|
}
|
|
401
451
|
|
|
452
|
+
/** One page of your jobs and crawls, newest first. Pass `nextCursor` back as `cursor`. */
|
|
453
|
+
async listJobs(options: ListOptions & CallOptions = {}): Promise<JobsPage> {
|
|
454
|
+
const body = record(await this.send(core.listJobsRequest(options), options));
|
|
455
|
+
const next = typeof body.next_cursor === "string" ? body.next_cursor : undefined;
|
|
456
|
+
return {
|
|
457
|
+
jobs: array(body.jobs).map((raw) => {
|
|
458
|
+
const job = record(raw) as unknown as JobSummary;
|
|
459
|
+
return { ...job, ...jobStatus(job) };
|
|
460
|
+
}),
|
|
461
|
+
...(next ? { nextCursor: next } : {}),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* One page of your tasks, newest first, including those of jobs and crawls.
|
|
467
|
+
* Items carry no result: fetch it with {@link getTask}.
|
|
468
|
+
*/
|
|
469
|
+
async listTasks(options: ListTasksOptions & CallOptions = {}): Promise<TasksPage> {
|
|
470
|
+
const body = record(await this.send(core.listTasksRequest(options), options));
|
|
471
|
+
const next = typeof body.next_cursor === "string" ? body.next_cursor : undefined;
|
|
472
|
+
return {
|
|
473
|
+
tasks: array(body.tasks).map((raw) => {
|
|
474
|
+
const task = record(raw) as unknown as TaskSummary;
|
|
475
|
+
return { ...task, job_id: task.job_id ?? null };
|
|
476
|
+
}),
|
|
477
|
+
...(next ? { nextCursor: next } : {}),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
402
481
|
/** Poll until the job is done. Without `timeoutMs` it waits indefinitely. */
|
|
403
482
|
async waitJob(jobId: string, options: CallOptions = {}): Promise<JobStatus> {
|
|
404
483
|
// The timeout bounds the whole wait, not each poll.
|
|
@@ -434,21 +513,108 @@ export class DataFuel {
|
|
|
434
513
|
return this.jobResults(id, options);
|
|
435
514
|
}
|
|
436
515
|
|
|
516
|
+
/** Create a search job, wait for it, and return its results. */
|
|
517
|
+
async runSearchJob(
|
|
518
|
+
queries: string[],
|
|
519
|
+
options: SearchOptions & CallOptions & { sequential?: boolean } = {},
|
|
520
|
+
): Promise<JobResults> {
|
|
521
|
+
const id = await this.createSearchJob(queries, options);
|
|
522
|
+
await this.waitJob(id, options);
|
|
523
|
+
return this.jobResults(id, options);
|
|
524
|
+
}
|
|
525
|
+
|
|
437
526
|
// --- account -----------------------------------------------------------
|
|
438
527
|
|
|
439
|
-
/** Which task types and LLM engines are switched on right now. */
|
|
528
|
+
/** Which task types and LLM engines are switched on right now. Needs no key. */
|
|
440
529
|
async capabilities(options: CallOptions = {}): Promise<Capabilities> {
|
|
441
|
-
|
|
442
|
-
|
|
530
|
+
const request = new core.Request(
|
|
531
|
+
"GET",
|
|
532
|
+
"/config/capabilities",
|
|
533
|
+
undefined,
|
|
534
|
+
undefined,
|
|
535
|
+
undefined,
|
|
536
|
+
false,
|
|
537
|
+
);
|
|
538
|
+
return new Capabilities(record(await this.send(request, options)));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** The browser actions `jsInstructions` accepts, with their arguments. Needs no key. */
|
|
542
|
+
async jsInstructions(options: CallOptions = {}): Promise<JsInstruction[]> {
|
|
543
|
+
const request = new core.Request(
|
|
544
|
+
"GET",
|
|
545
|
+
"/config/js-instructions",
|
|
546
|
+
undefined,
|
|
547
|
+
undefined,
|
|
548
|
+
undefined,
|
|
549
|
+
false,
|
|
443
550
|
);
|
|
551
|
+
const body = record(await this.send(request, options));
|
|
552
|
+
return Array.isArray(body.instructions) ? (body.instructions as JsInstruction[]) : [];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Countries, regions and cities a proxy type can exit from. */
|
|
556
|
+
async proxyLocations(
|
|
557
|
+
options: CallOptions & { proxyType?: string } = {},
|
|
558
|
+
): Promise<ProxyCountry[]> {
|
|
559
|
+
const params = options.proxyType ? { proxy_type: options.proxyType } : undefined;
|
|
560
|
+
return list(
|
|
561
|
+
await this.send(new core.Request("GET", "/config/proxy/locations", params), options),
|
|
562
|
+
) as ProxyCountry[];
|
|
444
563
|
}
|
|
445
564
|
|
|
446
|
-
/**
|
|
565
|
+
/** ASNs a proxy type can exit from in one country (ISO 3166-1 alpha-2). */
|
|
566
|
+
async proxyAsns(
|
|
567
|
+
country: string,
|
|
568
|
+
options: CallOptions & { proxyType?: string } = {},
|
|
569
|
+
): Promise<ProxyLocation[]> {
|
|
570
|
+
const params: Record<string, string> = { country };
|
|
571
|
+
if (options.proxyType) params.proxy_type = options.proxyType;
|
|
572
|
+
return list(
|
|
573
|
+
await this.send(new core.Request("GET", "/config/proxy/asn", params), options),
|
|
574
|
+
) as ProxyLocation[];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Remaining credits: plan and pay-as-you-go together. */
|
|
447
578
|
async balance(options: CallOptions = {}): Promise<number> {
|
|
448
579
|
const body = await this.send(new core.Request("GET", "/users/@me/balance"), options);
|
|
449
580
|
return intField(body, "balance");
|
|
450
581
|
}
|
|
451
582
|
|
|
583
|
+
/** Remaining credits by pool: plan credits (spent first) and pay-as-you-go credits. */
|
|
584
|
+
async balanceSplit(options: CallOptions = {}): Promise<BalanceSplit> {
|
|
585
|
+
const body = await this.send(new core.Request("GET", "/users/@me/balance"), options);
|
|
586
|
+
return {
|
|
587
|
+
balance: intField(body, "balance"),
|
|
588
|
+
plan_balance: intField(body, "plan_balance"),
|
|
589
|
+
payg_balance: intField(body, "payg_balance"),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Credit movements, newest first: plan assignments, credit pack purchases, usage, refunds, expiry.
|
|
595
|
+
* `sums` totals each operation over the whole range, not just this page.
|
|
596
|
+
*/
|
|
597
|
+
async transactions(options: TransactionsOptions & CallOptions = {}): Promise<TransactionsPage> {
|
|
598
|
+
const body = record(await this.send(core.transactionsRequest(options), options));
|
|
599
|
+
return {
|
|
600
|
+
transactions: array(body.transactions) as TransactionsPage["transactions"],
|
|
601
|
+
total_count: Number(body.total_count ?? 0),
|
|
602
|
+
sums: array(body.sums) as TransactionsPage["sums"],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Usage over a date range: totals, a time series and breakdowns. Default: the last 30 days. */
|
|
607
|
+
async analytics(options: AnalyticsOptions & CallOptions = {}): Promise<Analytics> {
|
|
608
|
+
const body = record(await this.send(core.analyticsRequest(options), options));
|
|
609
|
+
return {
|
|
610
|
+
...(body as unknown as Analytics),
|
|
611
|
+
timeseries: array(body.timeseries) as Analytics["timeseries"],
|
|
612
|
+
by_module: array(body.by_module) as Analytics["by_module"],
|
|
613
|
+
top_targets: array(body.top_targets) as Analytics["top_targets"],
|
|
614
|
+
by_status_code: array(body.by_status_code) as Analytics["by_status_code"],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
452
618
|
/** The account behind the API key. */
|
|
453
619
|
async me(options: CallOptions = {}): Promise<Profile> {
|
|
454
620
|
const body = await this.send(new core.Request("GET", "/users/@me"), options);
|
|
@@ -504,6 +670,38 @@ function record(body: unknown): Record<string, unknown> {
|
|
|
504
670
|
return body as Record<string, unknown>;
|
|
505
671
|
}
|
|
506
672
|
|
|
673
|
+
function list(body: unknown): unknown[] {
|
|
674
|
+
if (!Array.isArray(body)) {
|
|
675
|
+
throw new DataFuelError(`unexpected answer from the API: ${JSON.stringify(body) ?? "empty"}`);
|
|
676
|
+
}
|
|
677
|
+
return body;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function array(value: unknown): unknown[] {
|
|
681
|
+
return Array.isArray(value) ? value : [];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function jobStatus(body: unknown): JobStatus {
|
|
685
|
+
const raw = record(body) as unknown as Partial<JobStatus>;
|
|
686
|
+
return {
|
|
687
|
+
status: raw.status as JobStatus["status"],
|
|
688
|
+
tasks_count: raw.tasks_count ?? 0,
|
|
689
|
+
tasks_done: raw.tasks_done ?? 0,
|
|
690
|
+
tasks_remaining: raw.tasks_remaining ?? 0,
|
|
691
|
+
total_cost: raw.total_cost ?? 0,
|
|
692
|
+
done: isDone(raw.status),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function cancelResult(body: unknown): CancelResult {
|
|
697
|
+
const raw = record(body) as unknown as Partial<CancelResult>;
|
|
698
|
+
return {
|
|
699
|
+
...jobStatus(body),
|
|
700
|
+
refunded_tasks: raw.refunded_tasks ?? 0,
|
|
701
|
+
refunded_credits: raw.refunded_credits ?? 0,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
507
705
|
function field(body: unknown, name: string): unknown {
|
|
508
706
|
const value = record(body)[name];
|
|
509
707
|
if (value === undefined || value === null) {
|
package/src/core.ts
CHANGED
|
@@ -7,12 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { APIError, Unavailable } from "./errors.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
AI,
|
|
12
|
+
AnalyticsOptions,
|
|
13
|
+
Engine,
|
|
14
|
+
ListOptions,
|
|
15
|
+
ListTasksOptions,
|
|
16
|
+
Proxy,
|
|
17
|
+
ScrapeOptions,
|
|
18
|
+
TransactionsOptions,
|
|
19
|
+
} from "./models.js";
|
|
11
20
|
|
|
12
21
|
export const DEFAULT_BASE_URL = "https://scraping-api.datafuel.ai/api/v1";
|
|
13
22
|
|
|
14
23
|
/** Kept in step with package.json by a test; see test/hardening.test.ts. */
|
|
15
|
-
export const VERSION = "0.
|
|
24
|
+
export const VERSION = "0.3.0";
|
|
16
25
|
|
|
17
26
|
/** How long a synchronous call waits for a result before giving up. */
|
|
18
27
|
export const DEFAULT_TIMEOUT_MS = 180_000;
|
|
@@ -30,6 +39,7 @@ export class Request {
|
|
|
30
39
|
readonly params?: Record<string, string>,
|
|
31
40
|
readonly body?: Record<string, unknown>,
|
|
32
41
|
readonly idempotencyKey?: string,
|
|
42
|
+
readonly auth: boolean = true,
|
|
33
43
|
) {}
|
|
34
44
|
|
|
35
45
|
/** GETs are safe by nature, writes because they carry an idempotency key. */
|
|
@@ -75,10 +85,10 @@ export function headers(
|
|
|
75
85
|
request: Request,
|
|
76
86
|
): Record<string, string> {
|
|
77
87
|
const out: Record<string, string> = {
|
|
78
|
-
"X-API-Key": apiKey,
|
|
79
88
|
Accept: "application/json",
|
|
80
89
|
"User-Agent": userAgent,
|
|
81
90
|
};
|
|
91
|
+
if (apiKey) out["X-API-Key"] = apiKey;
|
|
82
92
|
if (request.body !== undefined) out["Content-Type"] = "application/json";
|
|
83
93
|
if (request.idempotencyKey !== undefined) out["Idempotency-Key"] = request.idempotencyKey;
|
|
84
94
|
return out;
|
|
@@ -215,7 +225,7 @@ export function buildJob(
|
|
|
215
225
|
sequential: boolean,
|
|
216
226
|
idempotencyKey: string,
|
|
217
227
|
): Request {
|
|
218
|
-
const attrs = { ...scrapeAttributes(options), urls: [...urls] };
|
|
228
|
+
const attrs = { ...scrapeAttributes(options), urls: [...urls], ...proxySession(proxy) };
|
|
219
229
|
return new Request(
|
|
220
230
|
"POST",
|
|
221
231
|
"/job",
|
|
@@ -231,6 +241,7 @@ export interface AskOptions {
|
|
|
231
241
|
websearch?: boolean;
|
|
232
242
|
followUp?: string;
|
|
233
243
|
country?: string;
|
|
244
|
+
location?: string;
|
|
234
245
|
format?: string;
|
|
235
246
|
}
|
|
236
247
|
|
|
@@ -243,6 +254,7 @@ export function askAttributes(
|
|
|
243
254
|
if (options.websearch) attrs.websearch = true;
|
|
244
255
|
if (options.followUp) attrs.follow_up_prompt = options.followUp;
|
|
245
256
|
if (options.country) attrs.proxy_country = options.country;
|
|
257
|
+
if (options.location) attrs.location = options.location;
|
|
246
258
|
if (options.format) attrs.result_format = options.format;
|
|
247
259
|
return attrs;
|
|
248
260
|
}
|
|
@@ -335,6 +347,7 @@ export function buildCrawl(
|
|
|
335
347
|
if (options.excludePaths?.length) attrs.exclude_paths = [...options.excludePaths];
|
|
336
348
|
if (options.includeSubdomains) attrs.include_subdomains = true;
|
|
337
349
|
if (options.allowBackwardLinks) attrs.allow_backward_links = true;
|
|
350
|
+
Object.assign(attrs, proxySession(proxy));
|
|
338
351
|
return new Request(
|
|
339
352
|
"POST",
|
|
340
353
|
"/crawl",
|
|
@@ -344,6 +357,103 @@ export function buildCrawl(
|
|
|
344
357
|
);
|
|
345
358
|
}
|
|
346
359
|
|
|
360
|
+
/** Options for `search`, the Google SERP module. Proxy type is not used. */
|
|
361
|
+
export interface SearchOptions {
|
|
362
|
+
/** Google `gl`, e.g. "us". */
|
|
363
|
+
country?: string;
|
|
364
|
+
/** Google `hl`, e.g. "en". */
|
|
365
|
+
language?: string;
|
|
366
|
+
/** Canonical location name. Give at most one of location, uule, lat/lon. */
|
|
367
|
+
location?: string;
|
|
368
|
+
/** 1-based, default 1. */
|
|
369
|
+
page?: number;
|
|
370
|
+
/** e.g. "google.de". */
|
|
371
|
+
googleDomain?: string;
|
|
372
|
+
uule?: string;
|
|
373
|
+
lat?: number;
|
|
374
|
+
lon?: number;
|
|
375
|
+
/** Metres around lat/lon or location, max 1000. */
|
|
376
|
+
radius?: number;
|
|
377
|
+
cr?: string;
|
|
378
|
+
lr?: string;
|
|
379
|
+
tbs?: string;
|
|
380
|
+
safe?: "active" | "off";
|
|
381
|
+
nfpr?: boolean;
|
|
382
|
+
filter?: boolean;
|
|
383
|
+
uds?: string;
|
|
384
|
+
kgmid?: string;
|
|
385
|
+
si?: string;
|
|
386
|
+
ludocid?: string;
|
|
387
|
+
lsig?: string;
|
|
388
|
+
ibp?: string;
|
|
389
|
+
/** Exit country of the request. */
|
|
390
|
+
proxyCountry?: string;
|
|
391
|
+
/** Default json. */
|
|
392
|
+
format?: "json" | "html" | "markdown";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function searchAttributes(
|
|
396
|
+
queryField: string,
|
|
397
|
+
query: string | string[],
|
|
398
|
+
options: SearchOptions,
|
|
399
|
+
): Record<string, unknown> {
|
|
400
|
+
const attrs: Record<string, unknown> = { [queryField]: query };
|
|
401
|
+
const strings: [keyof SearchOptions, string][] = [
|
|
402
|
+
["country", "country"],
|
|
403
|
+
["language", "language"],
|
|
404
|
+
["location", "location"],
|
|
405
|
+
["googleDomain", "google_domain"],
|
|
406
|
+
["uule", "uule"],
|
|
407
|
+
["cr", "cr"],
|
|
408
|
+
["lr", "lr"],
|
|
409
|
+
["tbs", "tbs"],
|
|
410
|
+
["safe", "safe"],
|
|
411
|
+
["uds", "uds"],
|
|
412
|
+
["kgmid", "kgmid"],
|
|
413
|
+
["si", "si"],
|
|
414
|
+
["ludocid", "ludocid"],
|
|
415
|
+
["lsig", "lsig"],
|
|
416
|
+
["ibp", "ibp"],
|
|
417
|
+
["proxyCountry", "proxy_country"],
|
|
418
|
+
["format", "result_format"],
|
|
419
|
+
];
|
|
420
|
+
for (const [option, attribute] of strings) {
|
|
421
|
+
if (options[option]) attrs[attribute] = options[option];
|
|
422
|
+
}
|
|
423
|
+
if (options.page) attrs.page = options.page;
|
|
424
|
+
if (options.radius) attrs.radius = options.radius;
|
|
425
|
+
if (options.lat !== undefined) attrs.lat = options.lat;
|
|
426
|
+
if (options.lon !== undefined) attrs.lon = options.lon;
|
|
427
|
+
if (options.nfpr !== undefined) attrs.nfpr = options.nfpr;
|
|
428
|
+
if (options.filter !== undefined) attrs.filter = options.filter;
|
|
429
|
+
return attrs;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function buildSearch(
|
|
433
|
+
query: string,
|
|
434
|
+
options: SearchOptions,
|
|
435
|
+
idempotencyKey: string,
|
|
436
|
+
): Request {
|
|
437
|
+
const attrs = searchAttributes("query", query, options);
|
|
438
|
+
return new Request("POST", "/task", undefined, envelope("serp", attrs), idempotencyKey);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function buildSearchJob(
|
|
442
|
+
queries: string[],
|
|
443
|
+
options: SearchOptions,
|
|
444
|
+
sequential: boolean,
|
|
445
|
+
idempotencyKey: string,
|
|
446
|
+
): Request {
|
|
447
|
+
const attrs = searchAttributes("queries", [...queries], options);
|
|
448
|
+
return new Request(
|
|
449
|
+
"POST",
|
|
450
|
+
"/job",
|
|
451
|
+
undefined,
|
|
452
|
+
envelope("serp", attrs, { multithreaded: !sequential }),
|
|
453
|
+
idempotencyKey,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
347
457
|
export function crawlResultsRequest(crawlId: string, cursor?: string, limit?: number): Request {
|
|
348
458
|
const params: Record<string, string> = {};
|
|
349
459
|
if (cursor) params.cursor = cursor;
|
|
@@ -355,6 +465,64 @@ export function crawlResultsRequest(crawlId: string, cursor?: string, limit?: nu
|
|
|
355
465
|
);
|
|
356
466
|
}
|
|
357
467
|
|
|
468
|
+
/** A Date as its UTC day, which is what the API's date filters take. */
|
|
469
|
+
export function day(value: string | Date): string {
|
|
470
|
+
return typeof value === "string" ? value : value.toISOString().slice(0, 10);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function query(
|
|
474
|
+
entries: [string, string | number | Date | undefined][],
|
|
475
|
+
): Record<string, string> | undefined {
|
|
476
|
+
const params: Record<string, string> = {};
|
|
477
|
+
for (const [name, value] of entries) {
|
|
478
|
+
if (value === undefined || value === "") continue;
|
|
479
|
+
params[name] = value instanceof Date ? day(value) : String(value);
|
|
480
|
+
}
|
|
481
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function listRequest(path: string, options: ListOptions, jobId?: string): Request {
|
|
485
|
+
const params = query([
|
|
486
|
+
["status", options.status],
|
|
487
|
+
["type", options.type],
|
|
488
|
+
["job_id", jobId],
|
|
489
|
+
["start_date", options.startDate],
|
|
490
|
+
["end_date", options.endDate],
|
|
491
|
+
["limit", options.limit],
|
|
492
|
+
["cursor", options.cursor],
|
|
493
|
+
]);
|
|
494
|
+
return new Request("GET", path, params);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function listJobsRequest(options: ListOptions): Request {
|
|
498
|
+
return listRequest("/job", options);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function listTasksRequest(options: ListTasksOptions): Request {
|
|
502
|
+
return listRequest("/task", options, options.jobId);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function transactionsRequest(options: TransactionsOptions): Request {
|
|
506
|
+
const params = query([
|
|
507
|
+
["operation", options.operation],
|
|
508
|
+
["start_date", options.startDate],
|
|
509
|
+
["end_date", options.endDate],
|
|
510
|
+
["page", options.page],
|
|
511
|
+
["limit", options.limit],
|
|
512
|
+
]);
|
|
513
|
+
return new Request("GET", "/users/@me/transactions", params);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function analyticsRequest(options: AnalyticsOptions): Request {
|
|
517
|
+
const params = query([
|
|
518
|
+
["start_date", options.startDate],
|
|
519
|
+
["end_date", options.endDate],
|
|
520
|
+
["interval", options.interval],
|
|
521
|
+
["module", options.module],
|
|
522
|
+
]);
|
|
523
|
+
return new Request("GET", "/task/analytics/dashboard", params);
|
|
524
|
+
}
|
|
525
|
+
|
|
358
526
|
/** Whether the API answered "the task is still running" instead of a result. */
|
|
359
527
|
export function stillProcessing(body: unknown): boolean {
|
|
360
528
|
return (
|
package/src/errors.ts
CHANGED
|
@@ -24,10 +24,47 @@ export class NoApiKey extends DataFuelError {}
|
|
|
24
24
|
/** The request never got an answer: DNS, connection, abort, read timeout. */
|
|
25
25
|
export class TransportError extends DataFuelError {}
|
|
26
26
|
|
|
27
|
+
/** The `code` of an API error. The API may add codes; unknown ones pass through. */
|
|
28
|
+
export type ErrorCode =
|
|
29
|
+
| "UNAUTHORIZED"
|
|
30
|
+
| "INVALID_API_KEY"
|
|
31
|
+
| "FORBIDDEN"
|
|
32
|
+
| "INSUFFICIENT_CREDITS"
|
|
33
|
+
| "RATE_LIMIT_EXCEEDED"
|
|
34
|
+
| "CONCURRENCY_LIMIT_REACHED"
|
|
35
|
+
| "INVALID_REQUEST_BODY"
|
|
36
|
+
| "INVALID_ATTRIBUTES"
|
|
37
|
+
| "MISSING_TARGET"
|
|
38
|
+
| "UNSUPPORTED_TASK_TYPE"
|
|
39
|
+
| "JOB_REQUIRES_MULTIPLE_TARGETS"
|
|
40
|
+
| "INVALID_IDEMPOTENCY_KEY"
|
|
41
|
+
| "IDEMPOTENCY_KEY_REUSED"
|
|
42
|
+
| "INVALID_TASK_ID"
|
|
43
|
+
| "INVALID_JOB_ID"
|
|
44
|
+
| "TASK_NOT_FOUND"
|
|
45
|
+
| "JOB_NOT_FOUND"
|
|
46
|
+
| "CRAWL_NOT_FOUND"
|
|
47
|
+
| "JOB_NOT_CANCELLABLE"
|
|
48
|
+
| "INVALID_CRAWL_PATTERN"
|
|
49
|
+
| "CRAWL_UNSUPPORTED_OPTION"
|
|
50
|
+
| "INVALID_CURSOR"
|
|
51
|
+
| "INVALID_QUERY_PARAM"
|
|
52
|
+
| "INVALID_PROXY_TYPE"
|
|
53
|
+
| "INVALID_COUNTRY"
|
|
54
|
+
| "INVALID_DATE_FORMAT"
|
|
55
|
+
| "INVALID_DATE_RANGE"
|
|
56
|
+
| "INVALID_INTERVAL"
|
|
57
|
+
| "MODULE_UNAVAILABLE"
|
|
58
|
+
| "ENGINE_UNAVAILABLE"
|
|
59
|
+
| "API_KEY_RESET_FAILED"
|
|
60
|
+
| "TASK_RESULT_TIMEOUT"
|
|
61
|
+
| "INTERNAL_ERROR"
|
|
62
|
+
| (string & {});
|
|
63
|
+
|
|
27
64
|
/** A non-2xx answer from the API itself. */
|
|
28
65
|
export class APIError extends DataFuelError {
|
|
29
66
|
readonly status: number;
|
|
30
|
-
readonly code:
|
|
67
|
+
readonly code: ErrorCode;
|
|
31
68
|
/** Seconds the API asked us to wait, from Retry-After. 0 when absent. */
|
|
32
69
|
readonly retryAfter: number;
|
|
33
70
|
|
|
@@ -41,6 +78,8 @@ export class APIError extends DataFuelError {
|
|
|
41
78
|
|
|
42
79
|
/** 401: the API key is missing or invalid. */
|
|
43
80
|
export class Unauthorized extends APIError {}
|
|
81
|
+
/** 403 FORBIDDEN: the account is inactive. */
|
|
82
|
+
export class Forbidden extends APIError {}
|
|
44
83
|
/** 404: unknown id, or one that belongs to another account. */
|
|
45
84
|
export class NotFound extends APIError {}
|
|
46
85
|
/** 429: the account's request rate or concurrency limit was reached. */
|
|
@@ -51,7 +90,9 @@ export class InsufficientCredits extends APIError {}
|
|
|
51
90
|
export class InvalidAttributes extends APIError {}
|
|
52
91
|
/** 422: the key was already used for a different request. */
|
|
53
92
|
export class IdempotencyKeyReused extends APIError {}
|
|
54
|
-
/**
|
|
93
|
+
/** 409 JOB_NOT_CANCELLABLE: the job or crawl already finished. */
|
|
94
|
+
export class JobNotCancellable extends APIError {}
|
|
95
|
+
/** 503: an operator switched something off, or a dependency is down. The message carries the reason. */
|
|
55
96
|
export class Unavailable extends APIError {}
|
|
56
97
|
/** 503 MODULE_UNAVAILABLE: this task type is switched off. Nothing was charged. */
|
|
57
98
|
export class ModuleUnavailable extends Unavailable {}
|
|
@@ -109,12 +150,16 @@ const BY_CODE: Record<string, new (s: number, c: string, m: string, r?: number)
|
|
|
109
150
|
INVALID_ATTRIBUTES: InvalidAttributes,
|
|
110
151
|
IDEMPOTENCY_KEY_REUSED: IdempotencyKeyReused,
|
|
111
152
|
INVALID_API_KEY: Unauthorized,
|
|
153
|
+
FORBIDDEN: Forbidden,
|
|
154
|
+
JOB_NOT_CANCELLABLE: JobNotCancellable,
|
|
112
155
|
};
|
|
113
156
|
|
|
114
157
|
const BY_STATUS: Record<number, new (s: number, c: string, m: string, r?: number) => APIError> = {
|
|
115
158
|
401: Unauthorized,
|
|
116
159
|
402: InsufficientCredits,
|
|
160
|
+
403: Forbidden,
|
|
117
161
|
404: NotFound,
|
|
162
|
+
409: JobNotCancellable,
|
|
118
163
|
422: IdempotencyKeyReused,
|
|
119
164
|
429: RateLimited,
|
|
120
165
|
503: Unavailable,
|