diff --git a/src/resource_clients/run.ts b/src/resource_clients/run.ts index 90ee493b..29e5fb4e 100644 --- a/src/resource_clients/run.ts +++ b/src/resource_clients/run.ts @@ -8,12 +8,15 @@ import { LogClient } from './log'; import { RequestQueueClient } from './request_queue'; import { ApiClientOptionsWithOptionalResourcePath } from '../base/api_client'; import { ResourceClient } from '../base/resource_client'; +import type { ApifyResponse } from '../http_client'; import { pluckData, parseDateFields, cast, } from '../utils'; +const RUN_CHARGE_IDEMPOTENCY_HEADER = 'idempotency-key'; + export class RunClient extends ResourceClient { /** * @hidden @@ -113,7 +116,7 @@ export class RunClient extends ResourceClient { return cast(parseDateFields(pluckData(response.data))); } - async update(newFields: RunUpdateOptions) : Promise { + async update(newFields: RunUpdateOptions): Promise { ow(newFields, ow.object); return this._update(newFields); @@ -138,6 +141,36 @@ export class RunClient extends ResourceClient { return cast(parseDateFields(pluckData(response.data))); } + /** + * https://docs.apify.com/api/v2#/reference/actor-runs/charge-run/charge-run + */ + async charge(options: RunChargeOptions): Promise>> { + ow(options, ow.object.exactShape({ + eventName: ow.string, + count: ow.optional.number, + idempotencyKey: ow.optional.string, + })); + + const count = options.count ?? 1; + /** To avoid duplicates during the same milisecond, doesn't need to by crypto-secure. */ + const randomSuffix = (Math.random() + 1).toString(36).slice(3, 8); + const idempotencyKey = options.idempotencyKey ?? `${this.id}-${options.eventName}-${Date.now()}-${randomSuffix}`; + + const request: AxiosRequestConfig = { + url: this._url('charge'), + method: 'POST', + data: { + eventName: options.eventName, + count, + }, + headers: { + [RUN_CHARGE_IDEMPOTENCY_HEADER]: idempotencyKey, + }, + }; + const response = await this.httpClient.call(request); + return response; + } + /** * Returns a promise that resolves with the finished Run object when the provided actor run finishes * or with the unfinished Run object when the `waitSecs` timeout lapses. The promise is NOT rejected @@ -221,7 +254,7 @@ export interface RunMetamorphOptions { } export interface RunUpdateOptions { statusMessage?: string; - isStatusMessageTerminal? : boolean; + isStatusMessageTerminal?: boolean; } export interface RunResurrectOptions { @@ -230,6 +263,15 @@ export interface RunResurrectOptions { timeout?: number; } +export type RunChargeOptions = { + /** Name of the event to charge. Must be defined in the Actor's pricing info else the API will throw. */ + eventName: string; + /** Defaults to 1 */ + count?: number; + /** Defaults to runId-eventName-timestamp */ + idempotencyKey?: string; +}; + export interface RunWaitForFinishOptions { /** * Maximum time to wait for the run to finish, in seconds. diff --git a/test/mock_server/routes/runs.js b/test/mock_server/routes/runs.js index 2debcbe0..17e8b1ec 100644 --- a/test/mock_server/routes/runs.js +++ b/test/mock_server/routes/runs.js @@ -15,6 +15,7 @@ const ROUTES = [ { id: 'run-keyValueStore', method: 'GET', path: '/:runId/key-value-store' }, { id: 'run-requestQueue', method: 'GET', path: '/:runId/request-queue' }, { id: 'run-log', method: 'GET', path: '/:runId/log', type: 'text' }, + { id: 'run-charge', method: 'POST', path: '/:runId/charge' }, ]; addRoutes(runs, ROUTES); diff --git a/test/runs.test.js b/test/runs.test.js index 70467ae8..a8dbd294 100644 --- a/test/runs.test.js +++ b/test/runs.test.js @@ -283,5 +283,14 @@ describe('Run methods', () => { expect(browserRes).toEqual(res); validateRequest({}, { runId }); }); + + test('charge() works', async () => { + const runId = 'some-run-id'; + + const res = await client.run(runId).charge({ eventName: 'some-event' }); + expect(res.status).toEqual(200); + + await expect(client.run(runId).charge()).rejects.toThrow('Expected argument to be of type `object` but received type `undefined`'); + }); }); });