Skip to content

Commit

Permalink
feat: Add charge method to the run client for "pay per event" (#613)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Jkuzz authored Dec 5, 2024
1 parent 4431554 commit 3d9c64d
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 2 deletions.
46 changes: 44 additions & 2 deletions src/resource_clients/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -113,7 +116,7 @@ export class RunClient extends ResourceClient {
return cast(parseDateFields(pluckData(response.data)));
}

async update(newFields: RunUpdateOptions) : Promise<ActorRun> {
async update(newFields: RunUpdateOptions): Promise<ActorRun> {
ow(newFields, ow.object);

return this._update(newFields);
Expand All @@ -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<ApifyResponse<Record<string, never>>> {
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
Expand Down Expand Up @@ -221,7 +254,7 @@ export interface RunMetamorphOptions {
}
export interface RunUpdateOptions {
statusMessage?: string;
isStatusMessageTerminal? : boolean;
isStatusMessageTerminal?: boolean;
}

export interface RunResurrectOptions {
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions test/mock_server/routes/runs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions test/runs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`');
});
});
});

0 comments on commit 3d9c64d

Please sign in to comment.