Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add charge method to the run client for "pay per event" #613

Merged
merged 9 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity - what's the expected return type of this method? I see that most of the RunClient methods return ActorRun object (which reflects the changes made by the call).

Is this return type (ApifyResponse<Record<string, never>>) a temporary thing before we figure out the actual API response shape, or is this the final thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this in person. The endpoint uses a memorized version of the run from the database for performance reasons, as the run's pricing model cannot change during the run. Therefore we cannot return the modified run object and the only important output is the response status code.

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`');
});
});
});
Loading