@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/dist/index.js CHANGED
@@ -23,6 +23,8 @@ var APIError = class extends DataFuelError {
23
23
  };
24
24
  var Unauthorized = class extends APIError {
25
25
  };
26
+ var Forbidden = class extends APIError {
27
+ };
26
28
  var NotFound = class extends APIError {
27
29
  };
28
30
  var RateLimited = class extends APIError {
@@ -33,6 +35,8 @@ var InvalidAttributes = class extends APIError {
33
35
  };
34
36
  var IdempotencyKeyReused = class extends APIError {
35
37
  };
38
+ var JobNotCancellable = class extends APIError {
39
+ };
36
40
  var Unavailable = class extends APIError {
37
41
  };
38
42
  var ModuleUnavailable = class extends Unavailable {
@@ -68,12 +72,16 @@ var BY_CODE = {
68
72
  INSUFFICIENT_CREDITS: InsufficientCredits,
69
73
  INVALID_ATTRIBUTES: InvalidAttributes,
70
74
  IDEMPOTENCY_KEY_REUSED: IdempotencyKeyReused,
71
- INVALID_API_KEY: Unauthorized
75
+ INVALID_API_KEY: Unauthorized,
76
+ FORBIDDEN: Forbidden,
77
+ JOB_NOT_CANCELLABLE: JobNotCancellable
72
78
  };
73
79
  var BY_STATUS = {
74
80
  401: Unauthorized,
75
81
  402: InsufficientCredits,
82
+ 403: Forbidden,
76
83
  404: NotFound,
84
+ 409: JobNotCancellable,
77
85
  422: IdempotencyKeyReused,
78
86
  429: RateLimited,
79
87
  503: Unavailable
@@ -94,24 +102,26 @@ function apiError(status, body, retryAfter = 0) {
94
102
 
95
103
  // src/core.ts
96
104
  var DEFAULT_BASE_URL = "https://scraping-api.datafuel.ai/api/v1";
97
- var VERSION = "0.1.0";
105
+ var VERSION = "0.2.0";
98
106
  var DEFAULT_TIMEOUT_MS = 18e4;
99
107
  var STILL_PROCESSING_DELAY_MS = 2e3;
100
108
  var STILL_PROCESSING = "TASK_STILL_PROCESSING";
101
109
  var MAX_IDEMPOTENCY_KEY = 255;
102
110
  var Request = class {
103
- constructor(method, path, params, body, idempotencyKey) {
111
+ constructor(method, path, params, body, idempotencyKey, auth = true) {
104
112
  this.method = method;
105
113
  this.path = path;
106
114
  this.params = params;
107
115
  this.body = body;
108
116
  this.idempotencyKey = idempotencyKey;
117
+ this.auth = auth;
109
118
  }
110
119
  method;
111
120
  path;
112
121
  params;
113
122
  body;
114
123
  idempotencyKey;
124
+ auth;
115
125
  /** GETs are safe by nature, writes because they carry an idempotency key. */
116
126
  get retryable() {
117
127
  return this.method === "GET" || this.idempotencyKey !== void 0;
@@ -138,10 +148,10 @@ function newIdempotencyKey() {
138
148
  }
139
149
  function headers(apiKey, userAgent, request) {
140
150
  const out = {
141
- "X-API-Key": apiKey,
142
151
  Accept: "application/json",
143
152
  "User-Agent": userAgent
144
153
  };
154
+ if (apiKey) out["X-API-Key"] = apiKey;
145
155
  if (request.body !== void 0) out["Content-Type"] = "application/json";
146
156
  if (request.idempotencyKey !== void 0) out["Idempotency-Key"] = request.idempotencyKey;
147
157
  return out;
@@ -237,7 +247,7 @@ function buildScrape(url, options, proxy, idempotencyKey) {
237
247
  );
238
248
  }
239
249
  function buildJob(urls, options, proxy, sequential, idempotencyKey) {
240
- const attrs = { ...scrapeAttributes(options), urls: [...urls] };
250
+ const attrs = { ...scrapeAttributes(options), urls: [...urls], ...proxySession(proxy) };
241
251
  return new Request(
242
252
  "POST",
243
253
  "/job",
@@ -251,6 +261,7 @@ function askAttributes(promptField, prompt, options) {
251
261
  if (options.websearch) attrs.websearch = true;
252
262
  if (options.followUp) attrs.follow_up_prompt = options.followUp;
253
263
  if (options.country) attrs.proxy_country = options.country;
264
+ if (options.location) attrs.location = options.location;
254
265
  if (options.format) attrs.result_format = options.format;
255
266
  return attrs;
256
267
  }
@@ -293,6 +304,7 @@ function buildCrawl(url, options, proxy, idempotencyKey) {
293
304
  if (options.excludePaths?.length) attrs.exclude_paths = [...options.excludePaths];
294
305
  if (options.includeSubdomains) attrs.include_subdomains = true;
295
306
  if (options.allowBackwardLinks) attrs.allow_backward_links = true;
307
+ Object.assign(attrs, proxySession(proxy));
296
308
  return new Request(
297
309
  "POST",
298
310
  "/crawl",
@@ -301,6 +313,52 @@ function buildCrawl(url, options, proxy, idempotencyKey) {
301
313
  idempotencyKey
302
314
  );
303
315
  }
316
+ function searchAttributes(queryField, query2, options) {
317
+ const attrs = { [queryField]: query2 };
318
+ const strings = [
319
+ ["country", "country"],
320
+ ["language", "language"],
321
+ ["location", "location"],
322
+ ["googleDomain", "google_domain"],
323
+ ["uule", "uule"],
324
+ ["cr", "cr"],
325
+ ["lr", "lr"],
326
+ ["tbs", "tbs"],
327
+ ["safe", "safe"],
328
+ ["uds", "uds"],
329
+ ["kgmid", "kgmid"],
330
+ ["si", "si"],
331
+ ["ludocid", "ludocid"],
332
+ ["lsig", "lsig"],
333
+ ["ibp", "ibp"],
334
+ ["proxyCountry", "proxy_country"],
335
+ ["format", "result_format"]
336
+ ];
337
+ for (const [option, attribute] of strings) {
338
+ if (options[option]) attrs[attribute] = options[option];
339
+ }
340
+ if (options.page) attrs.page = options.page;
341
+ if (options.radius) attrs.radius = options.radius;
342
+ if (options.lat !== void 0) attrs.lat = options.lat;
343
+ if (options.lon !== void 0) attrs.lon = options.lon;
344
+ if (options.nfpr !== void 0) attrs.nfpr = options.nfpr;
345
+ if (options.filter !== void 0) attrs.filter = options.filter;
346
+ return attrs;
347
+ }
348
+ function buildSearch(query2, options, idempotencyKey) {
349
+ const attrs = searchAttributes("query", query2, options);
350
+ return new Request("POST", "/task", void 0, envelope("serp", attrs), idempotencyKey);
351
+ }
352
+ function buildSearchJob(queries, options, sequential, idempotencyKey) {
353
+ const attrs = searchAttributes("queries", [...queries], options);
354
+ return new Request(
355
+ "POST",
356
+ "/job",
357
+ void 0,
358
+ envelope("serp", attrs, { multithreaded: !sequential }),
359
+ idempotencyKey
360
+ );
361
+ }
304
362
  function crawlResultsRequest(crawlId, cursor, limit) {
305
363
  const params = {};
306
364
  if (cursor) params.cursor = cursor;
@@ -311,6 +369,54 @@ function crawlResultsRequest(crawlId, cursor, limit) {
311
369
  Object.keys(params).length > 0 ? params : void 0
312
370
  );
313
371
  }
372
+ function day(value) {
373
+ return typeof value === "string" ? value : value.toISOString().slice(0, 10);
374
+ }
375
+ function query(entries) {
376
+ const params = {};
377
+ for (const [name, value] of entries) {
378
+ if (value === void 0 || value === "") continue;
379
+ params[name] = value instanceof Date ? day(value) : String(value);
380
+ }
381
+ return Object.keys(params).length > 0 ? params : void 0;
382
+ }
383
+ function listRequest(path, options, jobId) {
384
+ const params = query([
385
+ ["status", options.status],
386
+ ["type", options.type],
387
+ ["job_id", jobId],
388
+ ["start_date", options.startDate],
389
+ ["end_date", options.endDate],
390
+ ["limit", options.limit],
391
+ ["cursor", options.cursor]
392
+ ]);
393
+ return new Request("GET", path, params);
394
+ }
395
+ function listJobsRequest(options) {
396
+ return listRequest("/job", options);
397
+ }
398
+ function listTasksRequest(options) {
399
+ return listRequest("/task", options, options.jobId);
400
+ }
401
+ function transactionsRequest(options) {
402
+ const params = query([
403
+ ["operation", options.operation],
404
+ ["start_date", options.startDate],
405
+ ["end_date", options.endDate],
406
+ ["page", options.page],
407
+ ["limit", options.limit]
408
+ ]);
409
+ return new Request("GET", "/users/@me/transactions", params);
410
+ }
411
+ function analyticsRequest(options) {
412
+ const params = query([
413
+ ["start_date", options.startDate],
414
+ ["end_date", options.endDate],
415
+ ["interval", options.interval],
416
+ ["module", options.module]
417
+ ]);
418
+ return new Request("GET", "/task/analytics/dashboard", params);
419
+ }
314
420
  function stillProcessing(body) {
315
421
  return body !== null && typeof body === "object" && body.code === STILL_PROCESSING;
316
422
  }
@@ -474,7 +580,7 @@ var DataFuel = class {
474
580
  return new Promise((resolve) => setTimeout(resolve, ms));
475
581
  }
476
582
  async send(request, opts = {}) {
477
- if (!this.apiKey) {
583
+ if (!this.apiKey && request.auth) {
478
584
  throw new NoApiKey("no API key: pass one to the client or set DATAFUEL_API_KEY");
479
585
  }
480
586
  const url = new URL(this.baseUrl + request.path);
@@ -578,6 +684,11 @@ var DataFuel = class {
578
684
  const request = buildAsk(prompt, options, key(options.idempotencyKey));
579
685
  return this.runTask(request, options);
580
686
  }
687
+ /** Run a Google search and return the results page, parsed to JSON by default. */
688
+ async search(query2, options = {}) {
689
+ const request = buildSearch(query2, options, key(options.idempotencyKey));
690
+ return this.runTask(request, options);
691
+ }
581
692
  /**
582
693
  * List the URLs of a site without scraping them.
583
694
  *
@@ -602,6 +713,18 @@ var DataFuel = class {
602
713
  const request = buildCrawl(url, options, options.proxy, key(options.idempotencyKey));
603
714
  return strField(await this.send(request, options), "job_id");
604
715
  }
716
+ /**
717
+ * Stop a crawl. Queued pages are refunded, pages in flight finish and bill.
718
+ * Throws {@link JobNotCancellable} when it already finished.
719
+ */
720
+ async cancelCrawl(crawlId, options = {}) {
721
+ return cancelResult(
722
+ await this.send(
723
+ new Request("POST", `/crawl/${pathSegment(crawlId)}/cancel`),
724
+ options
725
+ )
726
+ );
727
+ }
605
728
  /** Return the progress of a crawl. */
606
729
  async getCrawl(crawlId, options = {}) {
607
730
  const body = record(
@@ -712,20 +835,30 @@ var DataFuel = class {
712
835
  );
713
836
  return strField(await this.send(request, options), "id");
714
837
  }
838
+ /** Queue a batch of Google searches and return the job id. */
839
+ async createSearchJob(queries, options = {}) {
840
+ const request = buildSearchJob(
841
+ queries,
842
+ options,
843
+ options.sequential ?? false,
844
+ key(options.idempotencyKey)
845
+ );
846
+ return strField(await this.send(request, options), "id");
847
+ }
715
848
  /** Return the progress of a job. */
716
849
  async getJob(jobId, options = {}) {
717
- const body = record(
850
+ return jobStatus(
718
851
  await this.send(new Request("GET", `/job/${pathSegment(jobId)}`), options)
719
852
  );
720
- const raw = body;
721
- return {
722
- status: raw.status,
723
- tasks_count: raw.tasks_count ?? 0,
724
- tasks_done: raw.tasks_done ?? 0,
725
- tasks_remaining: raw.tasks_remaining ?? 0,
726
- total_cost: raw.total_cost ?? 0,
727
- done: isDone(raw.status)
728
- };
853
+ }
854
+ /**
855
+ * Stop a job. Queued tasks are refunded, tasks in flight finish and bill.
856
+ * Throws {@link JobNotCancellable} when it already finished.
857
+ */
858
+ async cancelJob(jobId, options = {}) {
859
+ return cancelResult(
860
+ await this.send(new Request("POST", `/job/${pathSegment(jobId)}/cancel`), options)
861
+ );
729
862
  }
730
863
  /**
731
864
  * Return the per-task results of a job.
@@ -746,6 +879,33 @@ var DataFuel = class {
746
879
  tasks: tasks.map((task) => new Result(record(task)))
747
880
  };
748
881
  }
882
+ /** One page of your jobs and crawls, newest first. Pass `nextCursor` back as `cursor`. */
883
+ async listJobs(options = {}) {
884
+ const body = record(await this.send(listJobsRequest(options), options));
885
+ const next = typeof body.next_cursor === "string" ? body.next_cursor : void 0;
886
+ return {
887
+ jobs: array(body.jobs).map((raw) => {
888
+ const job = record(raw);
889
+ return { ...job, ...jobStatus(job) };
890
+ }),
891
+ ...next ? { nextCursor: next } : {}
892
+ };
893
+ }
894
+ /**
895
+ * One page of your tasks, newest first, including those of jobs and crawls.
896
+ * Items carry no result: fetch it with {@link getTask}.
897
+ */
898
+ async listTasks(options = {}) {
899
+ const body = record(await this.send(listTasksRequest(options), options));
900
+ const next = typeof body.next_cursor === "string" ? body.next_cursor : void 0;
901
+ return {
902
+ tasks: array(body.tasks).map((raw) => {
903
+ const task = record(raw);
904
+ return { ...task, job_id: task.job_id ?? null };
905
+ }),
906
+ ...next ? { nextCursor: next } : {}
907
+ };
908
+ }
749
909
  /** Poll until the job is done. Without `timeoutMs` it waits indefinitely. */
750
910
  async waitJob(jobId, options = {}) {
751
911
  const { timeoutMs, ...perPoll } = options;
@@ -771,11 +931,51 @@ var DataFuel = class {
771
931
  await this.waitJob(id, options);
772
932
  return this.jobResults(id, options);
773
933
  }
934
+ /** Create a search job, wait for it, and return its results. */
935
+ async runSearchJob(queries, options = {}) {
936
+ const id = await this.createSearchJob(queries, options);
937
+ await this.waitJob(id, options);
938
+ return this.jobResults(id, options);
939
+ }
774
940
  // --- account -----------------------------------------------------------
775
- /** Which task types and LLM engines are switched on right now. */
941
+ /** Which task types and LLM engines are switched on right now. Needs no key. */
776
942
  async capabilities(options = {}) {
777
- return new Capabilities(
778
- record(await this.send(new Request("GET", "/capabilities"), options))
943
+ const request = new Request(
944
+ "GET",
945
+ "/config/capabilities",
946
+ void 0,
947
+ void 0,
948
+ void 0,
949
+ false
950
+ );
951
+ return new Capabilities(record(await this.send(request, options)));
952
+ }
953
+ /** The browser actions `jsInstructions` accepts, with their arguments. Needs no key. */
954
+ async jsInstructions(options = {}) {
955
+ const request = new Request(
956
+ "GET",
957
+ "/config/js-instructions",
958
+ void 0,
959
+ void 0,
960
+ void 0,
961
+ false
962
+ );
963
+ const body = record(await this.send(request, options));
964
+ return Array.isArray(body.instructions) ? body.instructions : [];
965
+ }
966
+ /** Countries, regions and cities a proxy type can exit from. */
967
+ async proxyLocations(options = {}) {
968
+ const params = options.proxyType ? { proxy_type: options.proxyType } : void 0;
969
+ return list(
970
+ await this.send(new Request("GET", "/config/proxy/locations", params), options)
971
+ );
972
+ }
973
+ /** ASNs a proxy type can exit from in one country (ISO 3166-1 alpha-2). */
974
+ async proxyAsns(country, options = {}) {
975
+ const params = { country };
976
+ if (options.proxyType) params.proxy_type = options.proxyType;
977
+ return list(
978
+ await this.send(new Request("GET", "/config/proxy/asn", params), options)
779
979
  );
780
980
  }
781
981
  /** Remaining credits. */
@@ -783,6 +983,29 @@ var DataFuel = class {
783
983
  const body = await this.send(new Request("GET", "/users/@me/balance"), options);
784
984
  return intField(body, "balance");
785
985
  }
986
+ /**
987
+ * Credit movements, newest first: purchases, usage, refunds, expiry.
988
+ * `sums` totals each operation over the whole range, not just this page.
989
+ */
990
+ async transactions(options = {}) {
991
+ const body = record(await this.send(transactionsRequest(options), options));
992
+ return {
993
+ transactions: array(body.transactions),
994
+ total_count: Number(body.total_count ?? 0),
995
+ sums: array(body.sums)
996
+ };
997
+ }
998
+ /** Usage over a date range: totals, a time series and breakdowns. Default: the last 30 days. */
999
+ async analytics(options = {}) {
1000
+ const body = record(await this.send(analyticsRequest(options), options));
1001
+ return {
1002
+ ...body,
1003
+ timeseries: array(body.timeseries),
1004
+ by_module: array(body.by_module),
1005
+ top_targets: array(body.top_targets),
1006
+ by_status_code: array(body.by_status_code)
1007
+ };
1008
+ }
786
1009
  /** The account behind the API key. */
787
1010
  async me(options = {}) {
788
1011
  const body = await this.send(new Request("GET", "/users/@me"), options);
@@ -828,6 +1051,34 @@ function record(body) {
828
1051
  }
829
1052
  return body;
830
1053
  }
1054
+ function list(body) {
1055
+ if (!Array.isArray(body)) {
1056
+ throw new DataFuelError(`unexpected answer from the API: ${JSON.stringify(body) ?? "empty"}`);
1057
+ }
1058
+ return body;
1059
+ }
1060
+ function array(value) {
1061
+ return Array.isArray(value) ? value : [];
1062
+ }
1063
+ function jobStatus(body) {
1064
+ const raw = record(body);
1065
+ return {
1066
+ status: raw.status,
1067
+ tasks_count: raw.tasks_count ?? 0,
1068
+ tasks_done: raw.tasks_done ?? 0,
1069
+ tasks_remaining: raw.tasks_remaining ?? 0,
1070
+ total_cost: raw.total_cost ?? 0,
1071
+ done: isDone(raw.status)
1072
+ };
1073
+ }
1074
+ function cancelResult(body) {
1075
+ const raw = record(body);
1076
+ return {
1077
+ ...jobStatus(body),
1078
+ refunded_tasks: raw.refunded_tasks ?? 0,
1079
+ refunded_credits: raw.refunded_credits ?? 0
1080
+ };
1081
+ }
831
1082
  function field(body, name) {
832
1083
  const value = record(body)[name];
833
1084
  if (value === void 0 || value === null) {
@@ -854,9 +1105,11 @@ export {
854
1105
  DataFuel,
855
1106
  DataFuelError,
856
1107
  EngineUnavailable,
1108
+ Forbidden,
857
1109
  IdempotencyKeyReused,
858
1110
  InsufficientCredits,
859
1111
  InvalidAttributes,
1112
+ JobNotCancellable,
860
1113
  ModuleUnavailable,
861
1114
  NoApiKey,
862
1115
  NotFound,