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 retry policy targeting api limits (PL-000) #8

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"url": "https://github.com/voiceflow/nestjs-chargebee.git"
},
"scripts": {
"postinstall": "husky install",
"prepack": "pinst --disable",
"postpack": "pinst --enable",
"build": "yarn run-p build:cjs build:esm build:types",
Expand Down Expand Up @@ -46,7 +45,8 @@
]
},
"dependencies": {
"chargebee-typescript": "2.23.0"
"chargebee-typescript": "2.26.0",
"cockatiel": "^3.1.1"
},
"devDependencies": {
"@commitlint/cli": "^17.6.7",
Expand Down
19 changes: 19 additions & 0 deletions src/chargebee-resource-retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { retry, handleWhen, ExponentialBackoff } from "cockatiel";
import { isChargebeeErrorWithCode } from "./chargebee-resource.types";
import { ChargebeeRetryOptions } from "./chargebee.interface";

export const getChargebeeResourceRetryPolicy = (
options: ChargebeeRetryOptions,
) =>
retry(
options === false
? handleWhen(() => false)
: handleWhen(isChargebeeErrorWithCode("api_request_limit_exceeded")),
{
maxAttempts:
typeof options === "object" && "maxAttempts" in options
? options.maxAttempts
: 3,
backoff: new ExponentialBackoff(),
},
);
102 changes: 59 additions & 43 deletions src/chargebee-resource-wrapper.class.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ChargeBee } from "chargebee-typescript";

import type { ChargebeeResourceOptions } from "./chargebee-resource.interface";
import { ItemResource } from "./resources/item-resource";
import { AddonResource } from "./resources/addon-resource";
import { AddressResource } from "./resources/address-resource";
Expand Down Expand Up @@ -44,48 +45,63 @@ import { VirtualBankAccountResource } from "./resources/virtual-bank-account-res
import { SubscriptionResource } from "./resources/subscription-resource";

export class ChargebeeResourceWrapper {
constructor(private readonly client: ChargeBee) {}
constructor(
private readonly client: ChargeBee,
private readonly options: ChargebeeResourceOptions,
) {}

subscription = new SubscriptionResource(this.client);
customer = new CustomerResource(this.client);
paymentSource = new PaymentSourceResource(this.client);
virtualBankAccount = new VirtualBankAccountResource(this.client);
card = new CardResource(this.client);
promotionalCredit = new PromotionalCreditResource(this.client);
invoice = new InvoiceResource(this.client);
creditNote = new CreditNoteResource(this.client);
unbilledCharge = new UnbilledChargeResource(this.client);
order = new OrderResource(this.client);
gift = new GiftResource(this.client);
transaction = new TransactionResource(this.client);
hostedPage = new HostedPageResource(this.client);
estimate = new EstimateResource(this.client);
quote = new QuoteResource(this.client);
plan = new PlanResource(this.client);
addon = new AddonResource(this.client);
coupon = new CouponResource(this.client);
couponSet = new CouponSetResource(this.client);
couponCode = new CouponCodeResource(this.client);
address = new AddressResource(this.client);
usage = new UsageResource(this.client);
comment = new CommentResource(this.client);
portalSession = new PortalSessionResource(this.client);
siteMigrationDetail = new SiteMigrationDetailResource(this.client);
resourceMigration = new ResourceMigrationResource(this.client);
timeMachine = new TimeMachineResource(this.client);
export = new ExportResource(this.client);
paymentIntent = new PaymentIntentResource(this.client);
itemFamily = new ItemFamilyResource(this.client);
item = new ItemResource(this.client);
itemPrice = new ItemPriceResource(this.client);
attachedItem = new AttachedItemResource(this.client);
differentialPrice = new DifferentialPriceResource(this.client);
feature = new FeatureResource(this.client);
subscriptionEntitlement = new SubscriptionEntitlementResource(this.client);
itemEntitlement = new ItemEntitlementResource(this.client);
inAppSubscription = new InAppSubscriptionResource(this.client);
nonSubscription = new NonSubscriptionResource(this.client);
entitlementOverride = new EntitlementOverrideResource(this.client);
purchase = new PurchaseResource(this.client);
paymentVoucher = new PaymentVoucherResource(this.client);
subscription = new SubscriptionResource(this.client, this.options);
customer = new CustomerResource(this.client, this.options);
paymentSource = new PaymentSourceResource(this.client, this.options);
virtualBankAccount = new VirtualBankAccountResource(
this.client,
this.options,
);
card = new CardResource(this.client, this.options);
promotionalCredit = new PromotionalCreditResource(this.client, this.options);
invoice = new InvoiceResource(this.client, this.options);
creditNote = new CreditNoteResource(this.client, this.options);
unbilledCharge = new UnbilledChargeResource(this.client, this.options);
order = new OrderResource(this.client, this.options);
gift = new GiftResource(this.client, this.options);
transaction = new TransactionResource(this.client, this.options);
hostedPage = new HostedPageResource(this.client, this.options);
estimate = new EstimateResource(this.client, this.options);
quote = new QuoteResource(this.client, this.options);
plan = new PlanResource(this.client, this.options);
addon = new AddonResource(this.client, this.options);
coupon = new CouponResource(this.client, this.options);
couponSet = new CouponSetResource(this.client, this.options);
couponCode = new CouponCodeResource(this.client, this.options);
address = new AddressResource(this.client, this.options);
usage = new UsageResource(this.client, this.options);
comment = new CommentResource(this.client, this.options);
portalSession = new PortalSessionResource(this.client, this.options);
siteMigrationDetail = new SiteMigrationDetailResource(
this.client,
this.options,
);
resourceMigration = new ResourceMigrationResource(this.client, this.options);
timeMachine = new TimeMachineResource(this.client, this.options);
export = new ExportResource(this.client, this.options);
paymentIntent = new PaymentIntentResource(this.client, this.options);
itemFamily = new ItemFamilyResource(this.client, this.options);
item = new ItemResource(this.client, this.options);
itemPrice = new ItemPriceResource(this.client, this.options);
attachedItem = new AttachedItemResource(this.client, this.options);
differentialPrice = new DifferentialPriceResource(this.client, this.options);
feature = new FeatureResource(this.client, this.options);
subscriptionEntitlement = new SubscriptionEntitlementResource(
this.client,
this.options,
);
itemEntitlement = new ItemEntitlementResource(this.client, this.options);
inAppSubscription = new InAppSubscriptionResource(this.client, this.options);
nonSubscription = new NonSubscriptionResource(this.client, this.options);
entitlementOverride = new EntitlementOverrideResource(
this.client,
this.options,
);
purchase = new PurchaseResource(this.client, this.options);
paymentVoucher = new PaymentVoucherResource(this.client, this.options);
}
38 changes: 24 additions & 14 deletions src/chargebee-resource.class.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ChargeBee } from "chargebee-typescript";

import type { RequestWrapper } from "chargebee-typescript/lib/request_wrapper";
import type { ListResult } from "chargebee-typescript/lib/list_result";
import type { Result } from "chargebee-typescript/lib/result";
Expand All @@ -12,9 +11,16 @@ import {
type ResolveResultReturn,
isListOffsetOption,
} from "./chargebee-resource.types";
import { getChargebeeResourceRetryPolicy } from "./chargebee-resource-retry";
import type { ChargebeeResourceOptions } from "./chargebee-resource.interface";

export class ChargebeeResource {
constructor(protected readonly chargebee: ChargeBee) {}
constructor(
protected readonly chargebee: ChargeBee,
protected readonly options: ChargebeeResourceOptions,
) {}

private retryPolicy = getChargebeeResourceRetryPolicy(this.options.retry);

protected request<
TResourceName extends keyof ChargeBee,
Expand All @@ -40,9 +46,11 @@ export class ChargebeeResource {
] as MethodDefinition;

return async (...args: Parameters<MethodDefinition>) => {
return functionDef(...args)
.request()
.then(this.resolveResult(returning));
return this.retryPolicy.execute(() =>
functionDef(...args)
.request()
.then(this.resolveResult(returning)),
);
};
}

Expand Down Expand Up @@ -71,15 +79,17 @@ export class ChargebeeResource {
] as MethodDefinition;

const method = async (...args: Parameters<MethodDefinition>) => {
return functionDef(...args)
.request()
.then((listResult) => {
const items = listResult.list.map(this.resolveResult(returning));
return {
items,
nextOffset: listResult.next_offset as string | undefined,
};
});
return this.retryPolicy.execute(() =>
functionDef(...args)
.request()
.then((listResult) => {
const items = listResult.list.map(this.resolveResult(returning));
return {
items,
nextOffset: listResult.next_offset as string | undefined,
};
}),
);
};

const iterate = async function* (...args: Parameters<typeof method>) {
Expand Down
5 changes: 5 additions & 0 deletions src/chargebee-resource.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ChargebeeRetryOptions } from "./chargebee.interface";

export interface ChargebeeResourceOptions {
retry: ChargebeeRetryOptions;
}
44 changes: 44 additions & 0 deletions src/chargebee-resource.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ import type { ListResult } from "chargebee-typescript/lib/list_result";
import type { Result } from "chargebee-typescript/lib/result";
import type { ProcessWait } from "chargebee-typescript/lib/process_wait";

export interface ChargebeeError {
message: string;
type?: string;
api_error_code: ChargebeeErrorCode;
http_status_code: number;
}

export type ChargebeeErrorCode =
| "resource_not_found"
| "resource_limit_exhausted"
| "param_wrong_value"
| "duplicate_entry"
| "db_connection_failure"
| "invalid_state_for_request"
| "http_method_not_supported"
| "invalid_request"
| "resource_limit_exceeded"
| "unable_to_process_request"
| "lock_timeout"
| "internal_error"
| "internal_temporary_error"
| "request_blocked"
| "api_request_limit_exceeded"
| "third_party_api_request_limit_exceeded"
| "site_not_ready"
| "site_read_only_mode"
| "api_authentication_failed"
| "basic_authentication_failed"
| "api_authorization_failed"
| "site_not_found"
| "configuration_incompatible";

export const isChargebeeError = (arg: unknown): arg is ChargebeeError =>
arg != null &&
typeof arg === "object" &&
"message" in arg &&
"api_error_code" in arg &&
"http_status_code" in arg;

export const isChargebeeErrorWithCode =
<TCode extends ChargebeeErrorCode>(code: TCode) =>
(arg: unknown): arg is ChargebeeError & { api_error_code: TCode } =>
isChargebeeError(arg) && arg.api_error_code === code;

export type ResultMethodName<
TResource extends keyof ChargeBee,
TMethod extends keyof ChargeBee[TResource],
Expand Down
7 changes: 7 additions & 0 deletions src/chargebee.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export interface ChargebeeModuleOptions {
site: string;
apiKey: string;
retry?: ChargebeeRetryOptions;
}

export type ChargebeeRetryOptions =
| false
| {
maxAttempts?: number;
};
4 changes: 3 additions & 1 deletion src/chargebee.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export class ChargebeeService extends ChargebeeResourceWrapper {
@Optional()
client = configureChargebee(options),
) {
super(client);
super(client, {
retry: options.retry,
});
}
}

Expand Down
18 changes: 13 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3150,12 +3150,12 @@ __metadata:
languageName: node
linkType: hard

"chargebee-typescript@npm:2.23.0":
version: 2.23.0
resolution: "chargebee-typescript@npm:2.23.0"
"chargebee-typescript@npm:2.26.0":
version: 2.26.0
resolution: "chargebee-typescript@npm:2.26.0"
dependencies:
q: ">=1.0.1"
checksum: 19f147a10dd7cfc32b9da45fce6b9b3f052f7987c3865ac24ca8e0eb0f1f540bf2a4c937c4f5ff28c407cbf5aac97dd8fc0f746ac34c772da45a4604d3069983
checksum: e9647776d864f57c2615448467249345459aeebc1e002749a7f7720d15a726ebe1f9d25804db8c5c86c79526b67ff6c56bda6b8255721f7b91e04cee5d3ebc3c
languageName: node
linkType: hard

Expand Down Expand Up @@ -3317,6 +3317,13 @@ __metadata:
languageName: node
linkType: hard

"cockatiel@npm:^3.1.1":
version: 3.1.1
resolution: "cockatiel@npm:3.1.1"
checksum: c394fa5dc5a0f21a9ff9f007f16320a162000191c570fa277b527a72505a954aae5f2e93b0de0a558f5e3340fed37c014c9fe72d43adfee4aa09d976bdefe745
languageName: node
linkType: hard

"collect-v8-coverage@npm:^1.0.0":
version: 1.0.1
resolution: "collect-v8-coverage@npm:1.0.1"
Expand Down Expand Up @@ -7488,7 +7495,8 @@ __metadata:
"@types/supertest": ^2.0.12
"@typescript-eslint/eslint-plugin": ^6.2.1
"@typescript-eslint/parser": ^6.2.1
chargebee-typescript: 2.23.0
chargebee-typescript: 2.26.0
cockatiel: ^3.1.1
eslint: ^8.46.0
eslint-config-prettier: ^8.9.0
eslint-plugin-prettier: ^5.0.0
Expand Down