@datafuel/sdk 0.1.0 → 0.2.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,33 @@
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,
7
9
  CallOptions,
10
+ CancelResult,
8
11
  Capability,
9
12
  CrawlResult,
10
13
  CrawlResultsPage,
11
14
  CrawlStatus,
12
15
  JobResults,
16
+ JobsPage,
13
17
  JobStatus,
18
+ JobSummary,
19
+ JsInstruction,
20
+ ListOptions,
21
+ ListTasksOptions,
14
22
  Profile,
23
+ ProxyCountry,
24
+ ProxyLocation,
15
25
  ScrapeOptions,
16
26
  SiteMap,
27
+ TasksPage,
28
+ TaskSummary,
29
+ TransactionsOptions,
30
+ TransactionsPage,
17
31
  } from "./models.js";
18
32
  import { Capabilities, CrawlPage, isDone, Result } from "./models.js";
19
33
 
@@ -46,7 +60,8 @@ type Send = CallOptions & { timeoutMs?: number; signal?: AbortSignal };
46
60
  *
47
61
  * Pick the call by the shape of the work: one URL is {@link scrape}, a site's
48
62
  * 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}.
63
+ * of known URLs is {@link runJob}, a question for an AI engine is {@link ask}, a
64
+ * Google search is {@link search}.
50
65
  *
51
66
  * Every write carries an `Idempotency-Key`, generated per request, so a retry
52
67
  * attaches to the task already running instead of charging twice.
@@ -84,7 +99,7 @@ export class DataFuel {
84
99
  }
85
100
 
86
101
  private async send(request: core.Request, opts: Send = {}): Promise<unknown> {
87
- if (!this.apiKey) {
102
+ if (!this.apiKey && request.auth) {
88
103
  throw new NoApiKey("no API key: pass one to the client or set DATAFUEL_API_KEY");
89
104
  }
90
105
  const url = new URL(this.baseUrl + request.path);
@@ -205,6 +220,12 @@ export class DataFuel {
205
220
  return this.runTask(request, options);
206
221
  }
207
222
 
223
+ /** Run a Google search and return the results page, parsed to JSON by default. */
224
+ async search(query: string, options: SearchOptions & CallOptions = {}): Promise<Result> {
225
+ const request = core.buildSearch(query, options, core.key(options.idempotencyKey));
226
+ return this.runTask(request, options);
227
+ }
228
+
208
229
  /**
209
230
  * List the URLs of a site without scraping them.
210
231
  *
@@ -232,6 +253,19 @@ export class DataFuel {
232
253
  return strField(await this.send(request, options), "job_id");
233
254
  }
234
255
 
256
+ /**
257
+ * Stop a crawl. Queued pages are refunded, pages in flight finish and bill.
258
+ * Throws {@link JobNotCancellable} when it already finished.
259
+ */
260
+ async cancelCrawl(crawlId: string, options: CallOptions = {}): Promise<CancelResult> {
261
+ return cancelResult(
262
+ await this.send(
263
+ new core.Request("POST", `/crawl/${core.pathSegment(crawlId)}/cancel`),
264
+ options,
265
+ ),
266
+ );
267
+ }
268
+
235
269
  /** Return the progress of a crawl. */
236
270
  async getCrawl(crawlId: string, options: CallOptions = {}): Promise<CrawlStatus> {
237
271
  const body = record(
@@ -363,20 +397,35 @@ export class DataFuel {
363
397
  return strField(await this.send(request, options), "id");
364
398
  }
365
399
 
400
+ /** Queue a batch of Google searches and return the job id. */
401
+ async createSearchJob(
402
+ queries: string[],
403
+ options: SearchOptions & CallOptions & { sequential?: boolean } = {},
404
+ ): Promise<string> {
405
+ const request = core.buildSearchJob(
406
+ queries,
407
+ options,
408
+ options.sequential ?? false,
409
+ core.key(options.idempotencyKey),
410
+ );
411
+ return strField(await this.send(request, options), "id");
412
+ }
413
+
366
414
  /** Return the progress of a job. */
367
415
  async getJob(jobId: string, options: CallOptions = {}): Promise<JobStatus> {
368
- const body = record(
416
+ return jobStatus(
369
417
  await this.send(new core.Request("GET", `/job/${core.pathSegment(jobId)}`), options),
370
418
  );
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
- };
419
+ }
420
+
421
+ /**
422
+ * Stop a job. Queued tasks are refunded, tasks in flight finish and bill.
423
+ * Throws {@link JobNotCancellable} when it already finished.
424
+ */
425
+ async cancelJob(jobId: string, options: CallOptions = {}): Promise<CancelResult> {
426
+ return cancelResult(
427
+ await this.send(new core.Request("POST", `/job/${core.pathSegment(jobId)}/cancel`), options),
428
+ );
380
429
  }
381
430
 
382
431
  /**
@@ -399,6 +448,35 @@ export class DataFuel {
399
448
  };
400
449
  }
401
450
 
451
+ /** One page of your jobs and crawls, newest first. Pass `nextCursor` back as `cursor`. */
452
+ async listJobs(options: ListOptions & CallOptions = {}): Promise<JobsPage> {
453
+ const body = record(await this.send(core.listJobsRequest(options), options));
454
+ const next = typeof body.next_cursor === "string" ? body.next_cursor : undefined;
455
+ return {
456
+ jobs: array(body.jobs).map((raw) => {
457
+ const job = record(raw) as unknown as JobSummary;
458
+ return { ...job, ...jobStatus(job) };
459
+ }),
460
+ ...(next ? { nextCursor: next } : {}),
461
+ };
462
+ }
463
+
464
+ /**
465
+ * One page of your tasks, newest first, including those of jobs and crawls.
466
+ * Items carry no result: fetch it with {@link getTask}.
467
+ */
468
+ async listTasks(options: ListTasksOptions & CallOptions = {}): Promise<TasksPage> {
469
+ const body = record(await this.send(core.listTasksRequest(options), options));
470
+ const next = typeof body.next_cursor === "string" ? body.next_cursor : undefined;
471
+ return {
472
+ tasks: array(body.tasks).map((raw) => {
473
+ const task = record(raw) as unknown as TaskSummary;
474
+ return { ...task, job_id: task.job_id ?? null };
475
+ }),
476
+ ...(next ? { nextCursor: next } : {}),
477
+ };
478
+ }
479
+
402
480
  /** Poll until the job is done. Without `timeoutMs` it waits indefinitely. */
403
481
  async waitJob(jobId: string, options: CallOptions = {}): Promise<JobStatus> {
404
482
  // The timeout bounds the whole wait, not each poll.
@@ -434,13 +512,65 @@ export class DataFuel {
434
512
  return this.jobResults(id, options);
435
513
  }
436
514
 
515
+ /** Create a search job, wait for it, and return its results. */
516
+ async runSearchJob(
517
+ queries: string[],
518
+ options: SearchOptions & CallOptions & { sequential?: boolean } = {},
519
+ ): Promise<JobResults> {
520
+ const id = await this.createSearchJob(queries, options);
521
+ await this.waitJob(id, options);
522
+ return this.jobResults(id, options);
523
+ }
524
+
437
525
  // --- account -----------------------------------------------------------
438
526
 
439
- /** Which task types and LLM engines are switched on right now. */
527
+ /** Which task types and LLM engines are switched on right now. Needs no key. */
440
528
  async capabilities(options: CallOptions = {}): Promise<Capabilities> {
441
- return new Capabilities(
442
- record(await this.send(new core.Request("GET", "/capabilities"), options)),
529
+ const request = new core.Request(
530
+ "GET",
531
+ "/config/capabilities",
532
+ undefined,
533
+ undefined,
534
+ undefined,
535
+ false,
443
536
  );
537
+ return new Capabilities(record(await this.send(request, options)));
538
+ }
539
+
540
+ /** The browser actions `jsInstructions` accepts, with their arguments. Needs no key. */
541
+ async jsInstructions(options: CallOptions = {}): Promise<JsInstruction[]> {
542
+ const request = new core.Request(
543
+ "GET",
544
+ "/config/js-instructions",
545
+ undefined,
546
+ undefined,
547
+ undefined,
548
+ false,
549
+ );
550
+ const body = record(await this.send(request, options));
551
+ return Array.isArray(body.instructions) ? (body.instructions as JsInstruction[]) : [];
552
+ }
553
+
554
+ /** Countries, regions and cities a proxy type can exit from. */
555
+ async proxyLocations(
556
+ options: CallOptions & { proxyType?: string } = {},
557
+ ): Promise<ProxyCountry[]> {
558
+ const params = options.proxyType ? { proxy_type: options.proxyType } : undefined;
559
+ return list(
560
+ await this.send(new core.Request("GET", "/config/proxy/locations", params), options),
561
+ ) as ProxyCountry[];
562
+ }
563
+
564
+ /** ASNs a proxy type can exit from in one country (ISO 3166-1 alpha-2). */
565
+ async proxyAsns(
566
+ country: string,
567
+ options: CallOptions & { proxyType?: string } = {},
568
+ ): Promise<ProxyLocation[]> {
569
+ const params: Record<string, string> = { country };
570
+ if (options.proxyType) params.proxy_type = options.proxyType;
571
+ return list(
572
+ await this.send(new core.Request("GET", "/config/proxy/asn", params), options),
573
+ ) as ProxyLocation[];
444
574
  }
445
575
 
446
576
  /** Remaining credits. */
@@ -449,6 +579,31 @@ export class DataFuel {
449
579
  return intField(body, "balance");
450
580
  }
451
581
 
582
+ /**
583
+ * Credit movements, newest first: purchases, usage, refunds, expiry.
584
+ * `sums` totals each operation over the whole range, not just this page.
585
+ */
586
+ async transactions(options: TransactionsOptions & CallOptions = {}): Promise<TransactionsPage> {
587
+ const body = record(await this.send(core.transactionsRequest(options), options));
588
+ return {
589
+ transactions: array(body.transactions) as TransactionsPage["transactions"],
590
+ total_count: Number(body.total_count ?? 0),
591
+ sums: array(body.sums) as TransactionsPage["sums"],
592
+ };
593
+ }
594
+
595
+ /** Usage over a date range: totals, a time series and breakdowns. Default: the last 30 days. */
596
+ async analytics(options: AnalyticsOptions & CallOptions = {}): Promise<Analytics> {
597
+ const body = record(await this.send(core.analyticsRequest(options), options));
598
+ return {
599
+ ...(body as unknown as Analytics),
600
+ timeseries: array(body.timeseries) as Analytics["timeseries"],
601
+ by_module: array(body.by_module) as Analytics["by_module"],
602
+ top_targets: array(body.top_targets) as Analytics["top_targets"],
603
+ by_status_code: array(body.by_status_code) as Analytics["by_status_code"],
604
+ };
605
+ }
606
+
452
607
  /** The account behind the API key. */
453
608
  async me(options: CallOptions = {}): Promise<Profile> {
454
609
  const body = await this.send(new core.Request("GET", "/users/@me"), options);
@@ -504,6 +659,38 @@ function record(body: unknown): Record<string, unknown> {
504
659
  return body as Record<string, unknown>;
505
660
  }
506
661
 
662
+ function list(body: unknown): unknown[] {
663
+ if (!Array.isArray(body)) {
664
+ throw new DataFuelError(`unexpected answer from the API: ${JSON.stringify(body) ?? "empty"}`);
665
+ }
666
+ return body;
667
+ }
668
+
669
+ function array(value: unknown): unknown[] {
670
+ return Array.isArray(value) ? value : [];
671
+ }
672
+
673
+ function jobStatus(body: unknown): JobStatus {
674
+ const raw = record(body) as unknown as Partial<JobStatus>;
675
+ return {
676
+ status: raw.status as JobStatus["status"],
677
+ tasks_count: raw.tasks_count ?? 0,
678
+ tasks_done: raw.tasks_done ?? 0,
679
+ tasks_remaining: raw.tasks_remaining ?? 0,
680
+ total_cost: raw.total_cost ?? 0,
681
+ done: isDone(raw.status),
682
+ };
683
+ }
684
+
685
+ function cancelResult(body: unknown): CancelResult {
686
+ const raw = record(body) as unknown as Partial<CancelResult>;
687
+ return {
688
+ ...jobStatus(body),
689
+ refunded_tasks: raw.refunded_tasks ?? 0,
690
+ refunded_credits: raw.refunded_credits ?? 0,
691
+ };
692
+ }
693
+
507
694
  function field(body: unknown, name: string): unknown {
508
695
  const value = record(body)[name];
509
696
  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.2.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,