@datafuel/sdk 0.3.0 → 0.4.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/core.ts CHANGED
@@ -21,7 +21,7 @@ import type {
21
21
  export const DEFAULT_BASE_URL = "https://scraping-api.datafuel.ai/api/v1";
22
22
 
23
23
  /** Kept in step with package.json by a test; see test/hardening.test.ts. */
24
- export const VERSION = "0.3.0";
24
+ export const VERSION = "0.4.0";
25
25
 
26
26
  /** How long a synchronous call waits for a result before giving up. */
27
27
  export const DEFAULT_TIMEOUT_MS = 180_000;
@@ -37,9 +37,11 @@ export class Request {
37
37
  readonly method: string,
38
38
  readonly path: string,
39
39
  readonly params?: Record<string, string>,
40
- readonly body?: Record<string, unknown>,
40
+ readonly body?: Record<string, unknown> | unknown[],
41
41
  readonly idempotencyKey?: string,
42
42
  readonly auth: boolean = true,
43
+ /** A 503 that carries a health report is an answer, not an error. */
44
+ readonly degradedOk: boolean = false,
43
45
  ) {}
44
46
 
45
47
  /** GETs are safe by nature, writes because they carry an idempotency key. */
@@ -109,23 +111,10 @@ function aiAttributes(ai: AI): Record<string, unknown> {
109
111
  return out;
110
112
  }
111
113
 
112
- /**
113
- * Catch a half-filled AI key before it costs a request. The API does not check
114
- * these, so a missing one would be spent on a scrape whose AI step then fails.
115
- */
114
+ /** The API answers 400 INVALID_ATTRIBUTES without a provider; say so before sending. */
116
115
  export function validateAI(ai: AI): void {
117
- const given = {
118
- provider: ai.provider !== undefined,
119
- model: ai.model !== undefined,
120
- apiKey: ai.apiKey !== undefined,
121
- };
122
- const present = Object.values(given).filter(Boolean).length;
123
- if (present > 0 && present < 3) {
124
- const missing = Object.entries(given)
125
- .filter(([, ok]) => !ok)
126
- .map(([name]) => name)
127
- .join(", ");
128
- throw new TypeError(`ai needs provider, model and apiKey together; missing: ${missing}`);
116
+ if (!ai.provider) {
117
+ throw new TypeError("ai needs a provider; aiProviders() lists the ones the API supports");
129
118
  }
130
119
  }
131
120
 
@@ -141,7 +130,12 @@ export function scrapeAttributes(options: ScrapeOptions = {}): Record<string, un
141
130
  if (options.waitFor) attrs.wait_for_selector = options.waitFor;
142
131
  if (options.waitForTimeoutMs) attrs.wait_for_selector_timeout_ms = options.waitForTimeoutMs;
143
132
  if (options.jsInstructions) attrs.js_instructions = options.jsInstructions;
144
- if (options.blockResource) attrs.block_resource = options.blockResource;
133
+ if (options.blockResource?.length) {
134
+ attrs.block_resource =
135
+ typeof options.blockResource === "string"
136
+ ? options.blockResource
137
+ : [...options.blockResource];
138
+ }
145
139
  if (options.mainContentOnly) attrs.main_content_only = true;
146
140
  if (options.includeImages !== undefined) attrs.include_images = options.includeImages;
147
141
  if (options.extract) attrs.extract_selector = selector(options.extract);
@@ -386,7 +380,10 @@ export interface SearchOptions {
386
380
  ludocid?: string;
387
381
  lsig?: string;
388
382
  ibp?: string;
389
- /** Exit country of the request. */
383
+ /**
384
+ * Accepted but not used yet: searches leave through DataFuel's own pool.
385
+ * Target a market with `country` and `language`.
386
+ */
390
387
  proxyCountry?: string;
391
388
  /** Default json. */
392
389
  format?: "json" | "html" | "markdown";
@@ -465,6 +462,18 @@ export function crawlResultsRequest(crawlId: string, cursor?: string, limit?: nu
465
462
  );
466
463
  }
467
464
 
465
+ export function healthRequest(deep: boolean): Request {
466
+ return new Request(
467
+ "GET",
468
+ "/healthz",
469
+ deep ? { deep: "1" } : undefined,
470
+ undefined,
471
+ undefined,
472
+ false,
473
+ true,
474
+ );
475
+ }
476
+
468
477
  /** A Date as its UTC day, which is what the API's date filters take. */
469
478
  export function day(value: string | Date): string {
470
479
  return typeof value === "string" ? value : value.toISOString().slice(0, 10);
package/src/errors.ts CHANGED
@@ -45,6 +45,10 @@ export type ErrorCode =
45
45
  | "JOB_NOT_FOUND"
46
46
  | "CRAWL_NOT_FOUND"
47
47
  | "JOB_NOT_CANCELLABLE"
48
+ | "TASK_ALREADY_EXISTS"
49
+ | "JOB_ALREADY_EXISTS"
50
+ | "URL_LENGTH_CANNOT_BE_ZERO"
51
+ | "ANALYTICS_FETCH_FAILED"
48
52
  | "INVALID_CRAWL_PATTERN"
49
53
  | "CRAWL_UNSUPPORTED_OPTION"
50
54
  | "INVALID_CURSOR"
@@ -92,6 +96,12 @@ export class InvalidAttributes extends APIError {}
92
96
  export class IdempotencyKeyReused extends APIError {}
93
97
  /** 409 JOB_NOT_CANCELLABLE: the job or crawl already finished. */
94
98
  export class JobNotCancellable extends APIError {}
99
+ /**
100
+ * 409 TASK_ALREADY_EXISTS / JOB_ALREADY_EXISTS: the create collided with an
101
+ * existing task or job and is not an idempotent replay. Nothing was charged;
102
+ * send the request again.
103
+ */
104
+ export class AlreadyExists extends APIError {}
95
105
  /** 503: an operator switched something off, or a dependency is down. The message carries the reason. */
96
106
  export class Unavailable extends APIError {}
97
107
  /** 503 MODULE_UNAVAILABLE: this task type is switched off. Nothing was charged. */
@@ -152,6 +162,8 @@ const BY_CODE: Record<string, new (s: number, c: string, m: string, r?: number)
152
162
  INVALID_API_KEY: Unauthorized,
153
163
  FORBIDDEN: Forbidden,
154
164
  JOB_NOT_CANCELLABLE: JobNotCancellable,
165
+ TASK_ALREADY_EXISTS: AlreadyExists,
166
+ JOB_ALREADY_EXISTS: AlreadyExists,
155
167
  };
156
168
 
157
169
  const BY_STATUS: Record<number, new (s: number, c: string, m: string, r?: number) => APIError> = {
@@ -159,7 +171,6 @@ const BY_STATUS: Record<number, new (s: number, c: string, m: string, r?: number
159
171
  402: InsufficientCredits,
160
172
  403: Forbidden,
161
173
  404: NotFound,
162
- 409: JobNotCancellable,
163
174
  422: IdempotencyKeyReused,
164
175
  429: RateLimited,
165
176
  503: Unavailable,
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export type { ClientOptions } from "./client.js";
32
32
  export { DEFAULT_BASE_URL, VERSION } from "./core.js";
33
33
  export type { AskOptions, CrawlOptions, MapOptions, SearchOptions } from "./core.js";
34
34
  export {
35
+ AlreadyExists,
35
36
  APIError,
36
37
  Blocked,
37
38
  DataFuelError,
@@ -55,6 +56,7 @@ export type { ErrorCode } from "./errors.js";
55
56
  export { Capabilities, CrawlPage, isDone, Result } from "./models.js";
56
57
  export type {
57
58
  AI,
59
+ AIProvider,
58
60
  Analytics,
59
61
  AnalyticsCounts,
60
62
  AnalyticsOptions,
@@ -67,6 +69,7 @@ export type {
67
69
  CrawlStatus,
68
70
  Engine,
69
71
  Format,
72
+ Health,
70
73
  JobResults,
71
74
  JobsPage,
72
75
  JobStatus,
@@ -78,6 +81,7 @@ export type {
78
81
  ListTasksOptions,
79
82
  Payload,
80
83
  Profile,
84
+ ProtectionCheck,
81
85
  Proxy,
82
86
  ProxyCountry,
83
87
  ProxyLocation,
package/src/models.ts CHANGED
@@ -60,7 +60,9 @@ export interface AI {
60
60
  prompt?: string;
61
61
  /** Example JSON object the output must follow. */
62
62
  format?: unknown;
63
- provider?: "openai" | "anthropic" | "google";
63
+ /** Required. One of the providers `df.aiProviders()` lists; the API answers 400 INVALID_ATTRIBUTES without it. */
64
+ provider?: string;
65
+ /** One of the models `df.aiProviders()` lists for `provider`. Omit for the provider's default. */
64
66
  model?: string;
65
67
  apiKey?: string;
66
68
  }
@@ -75,9 +77,15 @@ export interface ScrapeOptions {
75
77
  jsRendering?: boolean;
76
78
  waitFor?: string;
77
79
  waitForTimeoutMs?: number;
78
- /** An object keyed by action, e.g. `{ click: "#more" }`. `df.jsInstructions()` lists them. */
80
+ /**
81
+ * Browser actions run after load: an array of single-action objects, run in
82
+ * order, e.g. `[{ click: "#more" }, { wait_ms: 1000 }, { click: "#more" }]`.
83
+ * An object keyed by action is still accepted, but its order is not
84
+ * guaranteed and an action cannot repeat. `df.jsInstructions()` lists them.
85
+ */
79
86
  jsInstructions?: unknown;
80
- blockResource?: string;
87
+ /** Resource types the browser must not load: one, e.g. `"Image"`, or several. */
88
+ blockResource?: string | string[];
81
89
  /** Markdown only: always render just the `<main>` / `<article>` container. */
82
90
  mainContentOnly?: boolean;
83
91
  /** Markdown only: `false` drops images and saves tokens. */
@@ -557,6 +565,28 @@ export interface JsInstruction {
557
565
  example: unknown;
558
566
  }
559
567
 
568
+ /** An LLM provider `ai.provider` accepts, with the models `ai.model` accepts for it. */
569
+ export interface AIProvider {
570
+ name: string;
571
+ models: string[];
572
+ }
573
+
574
+ /** The anti-bot protection in front of one host and path, from `checkProtection`. */
575
+ export interface ProtectionCheck {
576
+ host: string;
577
+ path: string;
578
+ /** cloudflare, cloudflare_5sec, akamai, imperva, perimeterx or unprotected. */
579
+ protection_type: string;
580
+ }
581
+
582
+ /** The state of the API. `checks` is filled by a deep check only, keyed by dependency. */
583
+ export interface Health {
584
+ status: "ok" | "degraded" | (string & {});
585
+ checks?: Record<string, { status: "ok" | "fail" | (string & {}); latency_ms: number }>;
586
+ /** Whether the API can serve requests. */
587
+ ok: boolean;
588
+ }
589
+
560
590
  /** A named proxy location: a city, or an ASN. */
561
591
  export interface ProxyLocation {
562
592
  code: string;