From 2767c8d18fcffe4ddb0d77105e3ac4acc2394a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobi=C3=A1=C5=A1=20Poto=C4=8Dek?= Date: Thu, 22 Feb 2024 10:54:25 +0100 Subject: [PATCH] feat: add monthlyUsage() and limits() endpoints to UserClients (#517) These endpoints have been around for some quite time and now we're exposing them via the JS client. --- src/resource_clients/user.ts | 133 +++++++++++++++++++++++++++++++ test/mock_server/routes/users.js | 3 +- test/users.test.js | 24 ++++++ 3 files changed, 159 insertions(+), 1 deletion(-) diff --git a/src/resource_clients/user.ts b/src/resource_clients/user.ts index b221bc8f..515d7f0e 100644 --- a/src/resource_clients/user.ts +++ b/src/resource_clients/user.ts @@ -1,5 +1,8 @@ +import { ApifyApiError } from '../apify_api_error'; import { ApiClientSubResourceOptions } from '../base/api_client'; import { ResourceClient } from '../base/resource_client'; +import { ApifyRequestConfig } from '../http_client'; +import { cast, catchNotFoundOrThrow, parseDateFields, pluckData } from '../utils'; export class UserClient extends ResourceClient { /** @@ -20,8 +23,50 @@ export class UserClient extends ResourceClient { async get(): Promise { return this._get() as Promise; } + + /** + * https://docs.apify.com/api/v2/#/reference/users/monthly-usage + */ + async monthlyUsage(): Promise { + const requestOpts: ApifyRequestConfig = { + url: this._url('usage/monthly'), + method: 'GET', + params: this._params(), + }; + try { + const response = await this.httpClient.call(requestOpts); + return cast(parseDateFields(pluckData(response.data))); + } catch (err) { + catchNotFoundOrThrow(err as ApifyApiError); + } + + return undefined; + } + + /** + * https://docs.apify.com/api/v2/#/reference/users/account-and-usage-limits + */ + async limits(): Promise { + const requestOpts: ApifyRequestConfig = { + url: this._url('limits'), + method: 'GET', + params: this._params(), + }; + try { + const response = await this.httpClient.call(requestOpts); + return cast(parseDateFields(pluckData(response.data))); + } catch (err) { + catchNotFoundOrThrow(err as ApifyApiError); + } + + return undefined; + } } +// +// Response interface for /users/:userId +// + export interface User { // Public properties username: string; @@ -83,3 +128,91 @@ export enum PlatformFeature { Proxy = 'PROXY', ProxyExternalAccess = 'PROXY_EXTERNAL_ACCESS', } + +// +// Response interface for /users/:userId/usage/monthly +// + +export interface MonthlyUsage { + usageCycle: UsageCycle; + monthlyServiceUsage: { [key: string]: MonthlyServiceUsageData }; + dailyServiceUsages: DailyServiceUsage[]; + totalUsageCreditsUsdBeforeVolumeDiscount: number; + totalUsageCreditsUsdAfterVolumeDiscount: number; +} + +export interface UsageCycle { + startAt: Date; + endAt: Date; +} + +/** Monthly usage of a single service */ +interface MonthlyServiceUsageData { + quantity: number; + baseAmountUsd: number; + baseUnitPriceUsd: number; + amountAfterVolumeDiscountUsd: number; + priceTiers: PriceTier[]; +} + +interface PriceTier { + quantityAbove: number; + discountPercent: number; + tierQuantity: number; + unitPriceUsd: number; + priceUsd: number; +} + +interface DailyServiceUsage { + date: Date; + serviceUsage: { [key: string]: DailyServiceUsageData }; + totalUsageCreditsUsd: number; +} + +/** Daily usage of a single service */ +interface DailyServiceUsageData { + quantity: number; + baseAmountUsd: number; +} + +// +// Response interface for /users/:userId/limits +// + +export interface AccountAndUsageLimits { + monthlyUsageCycle: MonthlyUsageCycle; + limits: Limits; + current: Current; +} + +export interface MonthlyUsageCycle { + startAt: Date; + endAt: Date; +} + +export interface Limits { + maxMonthlyUsageUsd: number; + maxMonthlyActorComputeUnits: number; + maxMonthlyExternalDataTransferGbytes: number; + maxMonthlyProxySerps: number; + maxMonthlyResidentialProxyGbytes: number; + maxActorMemoryGbytes: number; + maxActorCount: number; + maxActorTaskCount: number; + maxConcurrentActorJobs: number; + maxTeamAccountSeatCount: number; + dataRetentionDays: number; +} + +export interface Current { + monthlyUsageUsd: number; + monthlyActorComputeUnits: number; + monthlyExternalDataTransferGbytes: number; + monthlyProxySerps: number; + monthlyResidentialProxyGbytes: number; + actorMemoryGbytes: number; + actorCount: number; + actorTaskCount: number; + activeActorJobCount: number; + teamAccountSeatCount: number; +} diff --git a/test/mock_server/routes/users.js b/test/mock_server/routes/users.js index 1ac7ba83..6a8a43bf 100644 --- a/test/mock_server/routes/users.js +++ b/test/mock_server/routes/users.js @@ -6,7 +6,8 @@ const users = express.Router(); const ROUTES = [ { id: 'get-user', method: 'GET', path: '/:userId' }, - + { id: 'get-monthly-usage', method: 'GET', path: '/:userId/usage/monthly' }, + { id: 'get-limits', method: 'GET', path: '/:userId/limits' }, ]; addRoutes(users, ROUTES); diff --git a/test/users.test.js b/test/users.test.js index c52cf835..7a40fa11 100644 --- a/test/users.test.js +++ b/test/users.test.js @@ -58,5 +58,29 @@ describe('User methods', () => { expect(browserRes).toEqual(res); validateRequest({}, { userId: ME_USER_NAME_PLACEHOLDER }); }); + + test('monthlyUsage() works', async () => { + const userId = 'some-id'; + + const res = await client.user(userId).monthlyUsage(); + expect(res.id).toEqual('get-monthly-usage'); + validateRequest({}, { userId }); + + const browserRes = await page.evaluate((id) => client.user(id).monthlyUsage(), userId); + expect(browserRes).toEqual(res); + validateRequest({}, { userId }); + }); + + test('limits() works', async () => { + const userId = 'some-id'; + + const res = await client.user(userId).limits(); + expect(res.id).toEqual('get-limits'); + validateRequest({}, { userId }); + + const browserRes = await page.evaluate((id) => client.user(id).limits(), userId); + expect(browserRes).toEqual(res); + validateRequest({}, { userId }); + }); }); });