Skip to content

Commit 3d9c64d

Browse files
authored
feat: Add charge method to the run client for "pay per event" (#613)
Resolves apify/apify-core#18592 by adding the PPE charge endpoint to JS client. The idempotency key creation was taken from an actor by the store team. Issue to add docs URL #614
1 parent 4431554 commit 3d9c64d

File tree

3 files changed

+54
-2
lines changed

3 files changed

+54
-2
lines changed

src/resource_clients/run.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import { LogClient } from './log';
88
import { RequestQueueClient } from './request_queue';
99
import { ApiClientOptionsWithOptionalResourcePath } from '../base/api_client';
1010
import { ResourceClient } from '../base/resource_client';
11+
import type { ApifyResponse } from '../http_client';
1112
import {
1213
pluckData,
1314
parseDateFields,
1415
cast,
1516
} from '../utils';
1617

18+
const RUN_CHARGE_IDEMPOTENCY_HEADER = 'idempotency-key';
19+
1720
export class RunClient extends ResourceClient {
1821
/**
1922
* @hidden
@@ -113,7 +116,7 @@ export class RunClient extends ResourceClient {
113116
return cast(parseDateFields(pluckData(response.data)));
114117
}
115118

116-
async update(newFields: RunUpdateOptions) : Promise<ActorRun> {
119+
async update(newFields: RunUpdateOptions): Promise<ActorRun> {
117120
ow(newFields, ow.object);
118121

119122
return this._update(newFields);
@@ -138,6 +141,36 @@ export class RunClient extends ResourceClient {
138141
return cast(parseDateFields(pluckData(response.data)));
139142
}
140143

144+
/**
145+
* https://docs.apify.com/api/v2#/reference/actor-runs/charge-run/charge-run
146+
*/
147+
async charge(options: RunChargeOptions): Promise<ApifyResponse<Record<string, never>>> {
148+
ow(options, ow.object.exactShape({
149+
eventName: ow.string,
150+
count: ow.optional.number,
151+
idempotencyKey: ow.optional.string,
152+
}));
153+
154+
const count = options.count ?? 1;
155+
/** To avoid duplicates during the same milisecond, doesn't need to by crypto-secure. */
156+
const randomSuffix = (Math.random() + 1).toString(36).slice(3, 8);
157+
const idempotencyKey = options.idempotencyKey ?? `${this.id}-${options.eventName}-${Date.now()}-${randomSuffix}`;
158+
159+
const request: AxiosRequestConfig = {
160+
url: this._url('charge'),
161+
method: 'POST',
162+
data: {
163+
eventName: options.eventName,
164+
count,
165+
},
166+
headers: {
167+
[RUN_CHARGE_IDEMPOTENCY_HEADER]: idempotencyKey,
168+
},
169+
};
170+
const response = await this.httpClient.call(request);
171+
return response;
172+
}
173+
141174
/**
142175
* Returns a promise that resolves with the finished Run object when the provided actor run finishes
143176
* or with the unfinished Run object when the `waitSecs` timeout lapses. The promise is NOT rejected
@@ -221,7 +254,7 @@ export interface RunMetamorphOptions {
221254
}
222255
export interface RunUpdateOptions {
223256
statusMessage?: string;
224-
isStatusMessageTerminal? : boolean;
257+
isStatusMessageTerminal?: boolean;
225258
}
226259

227260
export interface RunResurrectOptions {
@@ -230,6 +263,15 @@ export interface RunResurrectOptions {
230263
timeout?: number;
231264
}
232265

266+
export type RunChargeOptions = {
267+
/** Name of the event to charge. Must be defined in the Actor's pricing info else the API will throw. */
268+
eventName: string;
269+
/** Defaults to 1 */
270+
count?: number;
271+
/** Defaults to runId-eventName-timestamp */
272+
idempotencyKey?: string;
273+
};
274+
233275
export interface RunWaitForFinishOptions {
234276
/**
235277
* Maximum time to wait for the run to finish, in seconds.

test/mock_server/routes/runs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const ROUTES = [
1515
{ id: 'run-keyValueStore', method: 'GET', path: '/:runId/key-value-store' },
1616
{ id: 'run-requestQueue', method: 'GET', path: '/:runId/request-queue' },
1717
{ id: 'run-log', method: 'GET', path: '/:runId/log', type: 'text' },
18+
{ id: 'run-charge', method: 'POST', path: '/:runId/charge' },
1819
];
1920

2021
addRoutes(runs, ROUTES);

test/runs.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,5 +283,14 @@ describe('Run methods', () => {
283283
expect(browserRes).toEqual(res);
284284
validateRequest({}, { runId });
285285
});
286+
287+
test('charge() works', async () => {
288+
const runId = 'some-run-id';
289+
290+
const res = await client.run(runId).charge({ eventName: 'some-event' });
291+
expect(res.status).toEqual(200);
292+
293+
await expect(client.run(runId).charge()).rejects.toThrow('Expected argument to be of type `object` but received type `undefined`');
294+
});
286295
});
287296
});

0 commit comments

Comments
 (0)