@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/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
- const body = record(
417
+ return jobStatus(
369
418
  await this.send(new core.Request("GET", `/job/${core.pathSegment(jobId)}`), options),
370
419
  );
371
- const raw = body as unknown as Partial<JobStatus>;
372
- return {
373
- status: raw.status as JobStatus["status"],
374
- tasks_count: raw.tasks_count ?? 0,
375
- tasks_done: raw.tasks_done ?? 0,
376
- tasks_remaining: raw.tasks_remaining ?? 0,
377
- total_cost: raw.total_cost ?? 0,
378
- done: isDone(raw.status),
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
- return new Capabilities(
442
- record(await this.send(new core.Request("GET", "/capabilities"), options)),
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
- /** Remaining credits. */
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 { AI, Engine, Proxy, ScrapeOptions } from "./models.js";
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.1.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: string;
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
- /** 503: an operator switched something off. The message carries the reason. */
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,