apify 4.0.0-beta.18 → 4.0.0-beta.20

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.
@@ -1,7 +1,10 @@
1
+ import { once } from 'node:events';
2
+ import { request as httpRequest } from 'node:http';
3
+ import { json } from 'node:stream/consumers';
1
4
  import { ProxyConfiguration as CoreProxyConfiguration } from '@crawlee/core';
2
- import { fetch, ProxyAgent } from 'undici';
3
5
  import { z } from 'zod';
4
6
  import { APIFY_ENV_VARS, APIFY_PROXY_VALUE_REGEX } from '@apify/consts';
7
+ import defaultLog from '@apify/log';
5
8
  import { cryptoRandomObjectId } from '@apify/utilities';
6
9
  import { Actor } from './actor.js';
7
10
  import { Configuration } from './configuration.js';
@@ -49,7 +52,7 @@ const SESSION_ID_LENGTH = 12;
49
52
  * @category Scaling
50
53
  */
51
54
  export class ProxyConfiguration extends CoreProxyConfiguration {
52
- config;
55
+ configuration;
53
56
  groups;
54
57
  countryCode;
55
58
  subdivisionCode;
@@ -57,17 +60,18 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
57
60
  hostname;
58
61
  port;
59
62
  usesApifyProxy;
63
+ log = defaultLog.child({ prefix: 'ProxyConfiguration' });
60
64
  /**
61
65
  * @internal
62
66
  */
63
- constructor(options = {}, config = Configuration.getGlobalConfig()) {
67
+ constructor(options = {}, configuration = Configuration.getGlobalConfiguration()) {
64
68
  const { proxyUrls, newUrlFunction, ...rest } = options;
65
69
  super({
66
70
  proxyUrls,
67
71
  newUrlFunction,
68
72
  ['validateRequired']: false,
69
73
  });
70
- this.config = config;
74
+ this.configuration = configuration;
71
75
  validate(z
72
76
  .object({
73
77
  groups: z.array(z.string().regex(APIFY_PROXY_VALUE_REGEX)).optional(),
@@ -79,12 +83,12 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
79
83
  password: z.string().optional(),
80
84
  })
81
85
  .strict(), rest);
82
- const { groups = [], apifyProxyGroups = [], countryCode, apifyProxyCountry, subdivisionCode, apifyProxySubdivision, password = config.proxyPassword, } = options;
86
+ const { groups = [], apifyProxyGroups = [], countryCode, apifyProxyCountry, subdivisionCode, apifyProxySubdivision, password = configuration.proxyPassword, } = options;
83
87
  const groupsToUse = groups.length ? groups : apifyProxyGroups;
84
88
  const countryCodeToUse = countryCode || apifyProxyCountry;
85
89
  const subdivisionCodeToUse = subdivisionCode || apifyProxySubdivision;
86
- const hostname = config.proxyHostname;
87
- const port = config.proxyPort;
90
+ const hostname = configuration.proxyHostname;
91
+ const port = configuration.proxyPort;
88
92
  // The Apify Proxy subdivision is expressed as part of the country
89
93
  // username parameter (`country-US_CA`), so a country is required.
90
94
  if (subdivisionCodeToUse && !countryCodeToUse) {
@@ -102,7 +106,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
102
106
  this.password = password;
103
107
  this.hostname = hostname;
104
108
  this.port = port;
105
- this.usesApifyProxy = !this.proxyUrls && !this.newUrlFunction;
109
+ this.usesApifyProxy = !proxyUrls && !newUrlFunction;
106
110
  if (proxyUrls && proxyUrls.some((url) => url?.includes('apify.com'))) {
107
111
  this.log.warning('Some Apify proxy features may work incorrectly. Please consider setting up Apify properties instead of `proxyUrls`.\n' +
108
112
  'See https://docs.apify.com/sdk/js/docs/concepts/proxy-management#apify-proxy-configuration');
@@ -172,7 +176,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
172
176
  * `proxyUrls`, the URLs are rotated round-robin.
173
177
  */
174
178
  async newUrl(options) {
175
- if (this.newUrlFunction || this.proxyUrls) {
179
+ if (!this.usesApifyProxy) {
176
180
  return super.newUrl(options);
177
181
  }
178
182
  return this.composeDefaultUrl(cryptoRandomObjectId(SESSION_ID_LENGTH));
@@ -208,7 +212,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
208
212
  */
209
213
  // TODO: Make this private
210
214
  async _setPasswordIfToken() {
211
- const { token } = this.config;
215
+ const { token } = this.configuration;
212
216
  if (!token)
213
217
  return;
214
218
  try {
@@ -239,6 +243,8 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
239
243
  return true;
240
244
  }
241
245
  const { connected, connectionError, isManInTheMiddle } = status;
246
+ // Declared `readonly false` on the base class; the status check is the one place that
247
+ // learns the actual value, so bypass the readonly marker.
242
248
  this.isManInTheMiddle = isManInTheMiddle;
243
249
  if (connected) {
244
250
  return true;
@@ -256,38 +262,56 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
256
262
  * Apify Proxy can be down for a second or a minute, but this should not crash processes.
257
263
  */
258
264
  async _fetchStatus() {
259
- const { proxyStatusUrl } = this.config;
260
- const url = `${proxyStatusUrl}/?format=json`;
261
- // The status endpoint (`proxy.apify.com`) is requested *through* the proxy
262
- // so it can report on this exact connection (auth + man-in-the-middle).
263
- // `undici`'s `fetch` + `ProxyAgent` come from the same package, so the
264
- // dispatcher is recognized (Node's global `fetch` uses a separate internal
265
- // copy of undici that would reject this dispatcher instance).
265
+ const { proxyStatusUrl } = this.configuration;
266
+ const statusUrl = `${proxyStatusUrl}/?format=json`;
266
267
  const proxyUrl = await this.newUrl();
267
268
  // Without a proxy URL we can't perform the (proxied) status check.
268
269
  if (!proxyUrl)
269
270
  return undefined;
270
- const dispatcher = new ProxyAgent(proxyUrl);
271
- try {
272
- for (let attempt = 1; attempt <= CHECK_ACCESS_MAX_ATTEMPTS; attempt++) {
273
- try {
274
- const response = await fetch(url, {
275
- dispatcher,
276
- signal: AbortSignal.timeout(CHECK_ACCESS_REQUEST_TIMEOUT_MILLIS),
277
- });
278
- if (!response.ok)
279
- continue;
280
- return (await response.json());
281
- }
282
- catch {
283
- // retry connection errors
284
- }
271
+ for (let attempt = 1; attempt <= CHECK_ACCESS_MAX_ATTEMPTS; attempt++) {
272
+ try {
273
+ return await this._requestStatus(statusUrl, proxyUrl);
274
+ }
275
+ catch {
276
+ // retry connection errors
285
277
  }
286
- return undefined;
287
278
  }
288
- finally {
289
- await dispatcher.close();
279
+ return undefined;
280
+ }
281
+ /**
282
+ * Fetches the Apify Proxy status endpoint once, *through* the proxy, so the
283
+ * response reports on this exact connection (auth + man-in-the-middle).
284
+ *
285
+ * Uses a native `node:http` forward-proxy request — an absolute request URL
286
+ * plus a `Proxy-Authorization` header — so no proxy-agent dependency is
287
+ * needed. The status endpoint (`http://proxy.apify.com`) is plain HTTP.
288
+ */
289
+ async _requestStatus(statusUrl, proxyUrl) {
290
+ const target = new URL(statusUrl);
291
+ const proxy = new URL(proxyUrl);
292
+ const headers = { host: target.host };
293
+ if (proxy.username) {
294
+ const credentials = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;
295
+ headers['proxy-authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`;
296
+ }
297
+ const request = httpRequest({
298
+ host: proxy.hostname,
299
+ port: proxy.port,
300
+ // Absolute-form request URI tells the proxy to forward the request.
301
+ path: target.href,
302
+ headers,
303
+ signal: AbortSignal.timeout(CHECK_ACCESS_REQUEST_TIMEOUT_MILLIS),
304
+ });
305
+ request.end();
306
+ // `once` rejects if the request emits `error` first (connection refused,
307
+ // timeout/abort), so failures propagate to the retry loop in `_fetchStatus`.
308
+ const [response] = (await once(request, 'response'));
309
+ const statusCode = response.statusCode ?? 0;
310
+ if (statusCode < 200 || statusCode >= 300) {
311
+ response.resume(); // drain so the socket can be freed
312
+ throw new Error(`Apify Proxy status check responded with status code ${statusCode}.`);
290
313
  }
314
+ return (await json(response));
291
315
  }
292
316
  /**
293
317
  * Throws cannot combine custom proxies with Apify Proxy
@@ -299,4 +323,11 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
299
323
  '"options.groups", "options.apifyProxyGroups", "options.countryCode", "options.apifyProxyCountry", ' +
300
324
  '"options.subdivisionCode" or "options.apifyProxySubdivision".');
301
325
  }
326
+ /**
327
+ * Throws cannot combine custom proxies with custom generating function
328
+ * @internal
329
+ */
330
+ _throwCannotCombineCustomMethods() {
331
+ throw new Error('Cannot combine custom proxies "options.proxyUrls" with custom generating function "options.newUrlFunction".');
332
+ }
302
333
  }
package/dist/storage.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Constructor, IStorage, StorageOpenOptions } from '@crawlee/core';
2
- import type { StorageClient } from '@crawlee/types';
1
+ import type { IStorage, StorageOpenOptions } from '@crawlee/core';
2
+ import type { Constructor, StorageBackend } from '@crawlee/types';
3
3
  import type { Configuration } from './configuration.js';
4
4
  export interface OpenStorageOptions {
5
5
  /**
@@ -47,7 +47,7 @@ export type StorageIdentifier = string | StorageAlias | StorageId | StorageName;
47
47
  export type StorageIdentifierWithoutAlias = string | StorageId | StorageName;
48
48
  export interface OpenStorageContext {
49
49
  config: Configuration;
50
- client?: StorageClient;
50
+ backend?: StorageBackend;
51
51
  purgedStorageAliases: Set<string>;
52
52
  }
53
53
  /**
package/dist/storage.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ApifyStorageClient } from './apify_storage_client.js';
1
+ import { ApifyStorageBackend } from './apify_storage_backend.js';
2
2
  const STORAGE_TYPE_KEYS = {
3
3
  Dataset: 'datasets',
4
4
  KeyValueStore: 'keyValueStores',
@@ -57,7 +57,7 @@ function resolveStorageIdentifier(storageType, identifier, config) {
57
57
  */
58
58
  export async function openStorage(storageClass, identifier, context) {
59
59
  const isAlias = identifier !== null && identifier !== undefined && typeof identifier === 'object' && 'alias' in identifier;
60
- if (isAlias && !context.config.isAtHome && context.client instanceof ApifyStorageClient) {
60
+ if (isAlias && !context.config.isAtHome && context.backend instanceof ApifyStorageBackend) {
61
61
  throw new Error('The `alias` option is not allowed for Apify-based storages running outside of Apify');
62
62
  }
63
63
  const resolvedIdOrName = resolveStorageIdentifier(storageClass.name, identifier, context.config);
@@ -69,11 +69,11 @@ export async function openStorage(storageClass, identifier, context) {
69
69
  !context.purgedStorageAliases.has(identifier.alias)) {
70
70
  context.purgedStorageAliases.add(identifier.alias);
71
71
  const existingStorage = await storageClass.open(resolvedIdOrName ?? null, {
72
- storageClient: context.client,
72
+ storageBackend: context.backend,
73
73
  });
74
74
  await existingStorage.drop();
75
75
  }
76
76
  return storageClass.open(resolvedIdOrName ?? null, {
77
- storageClient: context.client,
77
+ storageBackend: context.backend,
78
78
  });
79
79
  }
package/dist/utils.d.ts CHANGED
@@ -5,6 +5,11 @@ import type { z } from 'zod';
5
5
  * @internal
6
6
  */
7
7
  export declare function isNonEmptyObject(value: unknown): value is Record<string, unknown>;
8
+ /**
9
+ * Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
10
+ * @internal
11
+ */
12
+ export declare function snakeCaseToCamelCase(snakeCaseStr: string): string;
8
13
  /**
9
14
  * Error thrown when an argument fails validation (e.g. by `Actor.addWebhook()`
10
15
  * or the `ProxyConfiguration` constructor).
package/dist/utils.js CHANGED
@@ -20,6 +20,17 @@ const require = createRequire(import.meta.url);
20
20
  export function isNonEmptyObject(value) {
21
21
  return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
22
22
  }
23
+ /**
24
+ * Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
25
+ * @internal
26
+ */
27
+ export function snakeCaseToCamelCase(snakeCaseStr) {
28
+ return snakeCaseStr
29
+ .toLowerCase()
30
+ .split('_')
31
+ .map((part, index) => (index > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part))
32
+ .join('');
33
+ }
23
34
  /** Formats a zod issue path like `groups[0]` or `countryCode`. */
24
35
  function formatIssuePath(path) {
25
36
  let out = '';
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "apify",
3
- "version": "4.0.0-beta.18",
3
+ "version": "4.0.0-beta.20",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
- "node": ">=22.19.0"
6
+ "node": ">=22.0.0"
7
7
  },
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",
@@ -54,17 +54,17 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@apify/consts": "^2.51.0",
57
+ "@apify/datastructures": "^2.0.3",
57
58
  "@apify/input_secrets": "^1.2.0",
58
59
  "@apify/log": "^2.4.3",
59
60
  "@apify/timeout": "^0.3.0",
60
61
  "@apify/utilities": "^2.13.0",
61
- "@crawlee/core": "^4.0.0-beta.61",
62
- "@crawlee/types": "^4.0.0-beta.61",
63
- "@crawlee/utils": "^4.0.0-beta.61",
62
+ "@crawlee/core": "^4.0.0-beta.105",
63
+ "@crawlee/types": "^4.0.0-beta.105",
64
+ "@crawlee/utils": "^4.0.0-beta.105",
64
65
  "apify-client": "^2.23.4",
65
66
  "semver": "^7.5.4",
66
67
  "tslib": "^2.6.2",
67
- "undici": "^8.0.0",
68
68
  "ws": "^8.18.0",
69
69
  "zod": "^4.0.0"
70
70
  },
@@ -72,7 +72,6 @@
72
72
  "@apify/oxlint-config": "^0.2.5",
73
73
  "@apify/tsconfig": "^0.1.2",
74
74
  "@commitlint/config-conventional": "^21.0.0",
75
- "@crawlee/memory-storage": "^4.0.0-beta.61",
76
75
  "@playwright/browser-chromium": "^1.60.0",
77
76
  "@types/content-type": "^1.1.8",
78
77
  "@types/node": "^24.0.0",
@@ -80,7 +79,7 @@
80
79
  "@types/tough-cookie": "^4.0.5",
81
80
  "@types/ws": "^8.5.12",
82
81
  "commitlint": "^21.0.0",
83
- "crawlee": "^4.0.0-beta.61",
82
+ "crawlee": "^4.0.0-beta.105",
84
83
  "globby": "^16.0.0",
85
84
  "husky": "^9.1.7",
86
85
  "lint-staged": "^17.0.0",
@@ -1,66 +0,0 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
- import type { CreateDatasetClientOptions, CreateKeyValueStoreClientOptions, CreateRequestQueueClientOptions, DatasetClient, KeyValueStoreClient, RequestQueueClient, StorageClient } from '@crawlee/types';
3
- import type { ApifyClient } from 'apify-client';
4
- import { type ChargeResult, type ChargingManager } from './charging.js';
5
- import type { Configuration } from './configuration.js';
6
- type StorageType = 'Dataset' | 'KeyValueStore' | 'RequestQueue';
7
- /** Marks a dataset client whose `pushItems` charges for pay-per-event. @internal */
8
- export declare const USES_PUSH_DATA_INTERCEPTION: unique symbol;
9
- /**
10
- * Context of a single `Actor.pushData()` call, shared with the intercepted
11
- * `pushItems()` calls so they can (1) know which event to charge and
12
- * (2) aggregate the {@link ChargeResult} across the multiple `pushItems()`
13
- * calls a single `pushData()` may trigger (Crawlee batches large pushes).
14
- */
15
- export interface PpeAwarePushDataContext {
16
- eventName: string | undefined;
17
- chargeResult?: ChargeResult;
18
- }
19
- export declare const pushDataChargingContext: AsyncLocalStorage<PpeAwarePushDataContext>;
20
- /**
21
- * Bridges `apify-client`'s synchronous resource accessors (`dataset(id)`,
22
- * `keyValueStore(id)`, `requestQueue(id, options?)`) to crawlee v4's
23
- * `StorageClient` interface (async factory methods accepting either an `id`
24
- * or a `name`).
25
- *
26
- * For the run's default dataset it transparently swaps in a charging-aware
27
- * dataset client (pay-per-event on `Actor.pushData()`), provided a charging
28
- * manager is supplied and a default-dataset-item price is configured.
29
- *
30
- * `storageExists()` lets `Dataset.open(idOrName)` resolve a string to an id
31
- * first (when one exists on the platform) and fall back to a name otherwise —
32
- * otherwise crawlee's `resolveStorageIdentifier` treats every string as a name
33
- * and the SDK would silently create a new storage named like the passed id.
34
- *
35
- * `Actor` wires this up automatically; construct it directly only to use Apify
36
- * platform storage with crawlee's storage classes outside of `Actor` — e.g. to
37
- * read another run's output with an explicit token:
38
- *
39
- * ```ts
40
- * import { ApifyClient, ApifyStorageClient, Dataset } from 'apify';
41
- *
42
- * const client = new ApifyClient({ token });
43
- * const dataset = await Dataset.open(datasetId, { storageClient: new ApifyStorageClient(client) });
44
- * const { items } = await dataset.getData();
45
- * ```
46
- */
47
- export declare class ApifyStorageClient implements StorageClient {
48
- private readonly client;
49
- private readonly config?;
50
- private readonly getChargingManager?;
51
- constructor(client: ApifyClient, config?: Configuration | undefined, getChargingManager?: (() => ChargingManager) | undefined);
52
- storageExists(id: string, type: StorageType): Promise<boolean>;
53
- createDatasetClient(options?: CreateDatasetClientOptions): Promise<DatasetClient>;
54
- createKeyValueStoreClient(options?: CreateKeyValueStoreClientOptions): Promise<KeyValueStoreClient>;
55
- createRequestQueueClient(options?: CreateRequestQueueClientOptions): Promise<RequestQueueClient>;
56
- /**
57
- * Returns a charging-aware dataset client when `id` is the run's default
58
- * dataset and a default-dataset-item price is configured; otherwise
59
- * `undefined` (caller uses the plain client).
60
- */
61
- private chargingDatasetClient;
62
- private resolveId;
63
- private resourceClient;
64
- private collectionClient;
65
- }
66
- export {};
@@ -1,200 +0,0 @@
1
- /* eslint-disable max-classes-per-file */
2
- import { AsyncLocalStorage } from 'node:async_hooks';
3
- import { DatasetClient as ApifyDatasetClient } from 'apify-client';
4
- import { DEFAULT_DATASET_ITEM_EVENT, mergeChargeResults, pushDataAndCharge, } from './charging.js';
5
- const DEFAULT_ID_CONFIG_KEY = {
6
- Dataset: 'defaultDatasetId',
7
- KeyValueStore: 'defaultKeyValueStoreId',
8
- RequestQueue: 'defaultRequestQueueId',
9
- };
10
- /** Marks a dataset client whose `pushItems` charges for pay-per-event. @internal */
11
- export const USES_PUSH_DATA_INTERCEPTION = Symbol('apify:uses-push-data-interception');
12
- export const pushDataChargingContext = new AsyncLocalStorage();
13
- /**
14
- * Default `DatasetClient` that charges for pushed items (pay-per-event). Used
15
- * only for the run's default dataset when a `apify-default-dataset-item` price
16
- * is configured; for everything else the plain `apify-client` dataset client is
17
- * used.
18
- */
19
- class PpeAwareDatasetClient extends ApifyDatasetClient {
20
- getChargingManager;
21
- constructor(options, getChargingManager) {
22
- super(options);
23
- this.getChargingManager = getChargingManager;
24
- }
25
- normalizeItems(items) {
26
- if (typeof items === 'string') {
27
- const parsed = JSON.parse(items);
28
- return Array.isArray(parsed) ? parsed : [parsed];
29
- }
30
- if (Array.isArray(items)) {
31
- return items.flatMap((item) => typeof item === 'string' ? JSON.parse(item) : item);
32
- }
33
- return [items];
34
- }
35
- async pushItems(items) {
36
- const context = pushDataChargingContext.getStore();
37
- // A single JSON string may encode multiple items (e.g. '[{...},{...}]'),
38
- // which the charging logic would miscount — parse strings into arrays so
39
- // each logical item is counted individually.
40
- const normalizedItems = this.normalizeItems(items);
41
- const result = await pushDataAndCharge({
42
- chargingManager: this.getChargingManager(),
43
- items: normalizedItems,
44
- eventName: context?.eventName,
45
- isDefaultDataset: true,
46
- // stringify for faster validation in the Apify client
47
- pushFn: async (limitedItems) => super.pushItems(JSON.stringify(limitedItems)),
48
- });
49
- if (!context)
50
- return;
51
- // One `Actor.pushData()` may map to several `pushItems()` calls — aggregate.
52
- context.chargeResult =
53
- context.chargeResult === undefined ? result : mergeChargeResults(context.chargeResult, result);
54
- }
55
- }
56
- // crawlee v4's `StorageClient` sub-client interfaces use different method names
57
- // than `apify-client`'s resource clients (`getValue`/`getRecord`,
58
- // `pushData`/`pushItems`, `getData`/`listItems`, `getMetadata`/`get`,
59
- // `drop`/`delete`). `adapt` wraps a client in a name-remapping proxy: `renames`
60
- // aliases the differing methods and `overrides` replaces the few whose return
61
- // shape differs; everything else — identically-named methods and the
62
- // pay-per-event marker symbol — passes straight through.
63
- //
64
- // `purge()` has no apify-client equivalent and isn't needed on the platform
65
- // (a run's storages are already fresh), so it's a no-op.
66
- const noPurge = { purge: async () => { } };
67
- function adapt(client, renames, overrides = {}) {
68
- return new Proxy(client, {
69
- get(target, prop) {
70
- if (typeof prop === 'string' && prop in overrides)
71
- return overrides[prop];
72
- const value = Reflect.get(target, (typeof prop === 'string' && renames[prop]) || prop, target);
73
- return typeof value === 'function' ? value.bind(target) : value;
74
- },
75
- });
76
- }
77
- /**
78
- * Bridges `apify-client`'s synchronous resource accessors (`dataset(id)`,
79
- * `keyValueStore(id)`, `requestQueue(id, options?)`) to crawlee v4's
80
- * `StorageClient` interface (async factory methods accepting either an `id`
81
- * or a `name`).
82
- *
83
- * For the run's default dataset it transparently swaps in a charging-aware
84
- * dataset client (pay-per-event on `Actor.pushData()`), provided a charging
85
- * manager is supplied and a default-dataset-item price is configured.
86
- *
87
- * `storageExists()` lets `Dataset.open(idOrName)` resolve a string to an id
88
- * first (when one exists on the platform) and fall back to a name otherwise —
89
- * otherwise crawlee's `resolveStorageIdentifier` treats every string as a name
90
- * and the SDK would silently create a new storage named like the passed id.
91
- *
92
- * `Actor` wires this up automatically; construct it directly only to use Apify
93
- * platform storage with crawlee's storage classes outside of `Actor` — e.g. to
94
- * read another run's output with an explicit token:
95
- *
96
- * ```ts
97
- * import { ApifyClient, ApifyStorageClient, Dataset } from 'apify';
98
- *
99
- * const client = new ApifyClient({ token });
100
- * const dataset = await Dataset.open(datasetId, { storageClient: new ApifyStorageClient(client) });
101
- * const { items } = await dataset.getData();
102
- * ```
103
- */
104
- export class ApifyStorageClient {
105
- client;
106
- config;
107
- getChargingManager;
108
- constructor(client, config, getChargingManager) {
109
- this.client = client;
110
- this.config = config;
111
- this.getChargingManager = getChargingManager;
112
- }
113
- async storageExists(id, type) {
114
- // Apify's `GET /v2/{kind}/{idOrName}` matches by either id or name;
115
- // confirm it was an *id* match so crawlee can fall through to `{ name }`.
116
- const info = await this.resourceClient(id, type).get();
117
- return info?.id === id;
118
- }
119
- async createDatasetClient(options) {
120
- const id = await this.resolveId(options, 'Dataset');
121
- const client = this.chargingDatasetClient(id) ?? this.client.dataset(id);
122
- return adapt(client, {
123
- getMetadata: 'get',
124
- drop: 'delete',
125
- pushData: 'pushItems',
126
- getData: 'listItems',
127
- }, noPurge);
128
- }
129
- async createKeyValueStoreClient(options) {
130
- const id = await this.resolveId(options, 'KeyValueStore');
131
- const client = this.client.keyValueStore(id);
132
- return adapt(client, {
133
- getMetadata: 'get',
134
- getValue: 'getRecord',
135
- setValue: 'setRecord',
136
- deleteValue: 'deleteRecord',
137
- drop: 'delete',
138
- getPublicUrl: 'getRecordPublicUrl',
139
- }, {
140
- ...noPurge,
141
- // crawlee expects an array; apify-client returns `{ items }`.
142
- listKeys: async (opts) => (await client.listKeys(opts)).items,
143
- });
144
- }
145
- async createRequestQueueClient(options) {
146
- const id = await this.resolveId(options, 'RequestQueue');
147
- const client = this.client.requestQueue(id, options?.clientKey ? { clientKey: options.clientKey } : undefined);
148
- return adapt(client, { getMetadata: 'get', drop: 'delete' }, noPurge);
149
- }
150
- /**
151
- * Returns a charging-aware dataset client when `id` is the run's default
152
- * dataset and a default-dataset-item price is configured; otherwise
153
- * `undefined` (caller uses the plain client).
154
- */
155
- chargingDatasetClient(id) {
156
- const { getChargingManager } = this;
157
- if (!getChargingManager)
158
- return undefined;
159
- if (id !== this.config?.defaultDatasetId)
160
- return undefined;
161
- const hasDefaultDatasetItemEvent = DEFAULT_DATASET_ITEM_EVENT in getChargingManager().getPricingInfo().perEventPrices;
162
- if (!hasDefaultDatasetItemEvent)
163
- return undefined;
164
- const datasetClient = new PpeAwareDatasetClient({
165
- id,
166
- baseUrl: this.client.baseUrl,
167
- publicBaseUrl: this.client.publicBaseUrl,
168
- apifyClient: this.client,
169
- httpClient: this.client.httpClient,
170
- }, getChargingManager);
171
- Object.assign(datasetClient, {
172
- [USES_PUSH_DATA_INTERCEPTION]: true,
173
- });
174
- return datasetClient;
175
- }
176
- async resolveId(options, type) {
177
- if (options?.id)
178
- return options.id;
179
- if (options?.name) {
180
- return (await this.collectionClient(type).getOrCreate(options.name)).id;
181
- }
182
- // No id/name (crawlee's `__default__` alias): use the default storage
183
- // id from the run's environment. apify-client rejects an empty id.
184
- return this.config?.[DEFAULT_ID_CONFIG_KEY[type]] ?? '';
185
- }
186
- resourceClient(id, type) {
187
- if (type === 'Dataset')
188
- return this.client.dataset(id);
189
- if (type === 'KeyValueStore')
190
- return this.client.keyValueStore(id);
191
- return this.client.requestQueue(id);
192
- }
193
- collectionClient(type) {
194
- if (type === 'Dataset')
195
- return this.client.datasets();
196
- if (type === 'KeyValueStore')
197
- return this.client.keyValueStores();
198
- return this.client.requestQueues();
199
- }
200
- }