src/services/base.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import type Stripe from "stripe";
import {
BadRequest,
NotImplemented,
Unavailable,
NotAuthenticated,
TooManyRequests,
GeneralError,
PaymentError,
MethodNotAllowed
} from "@feathersjs/errors";
import { _ } from "@feathersjs/commons";
import type {
IUnderScoreFunctions,
ParamsWithStripe,
StripeServiceOptions
} from "../types";
import type { Params, Query } from "@feathersjs/feathers";
type FilteredParams<T extends ParamsWithStripe = ParamsWithStripe> = {
query: Remove$QueryKeys<T["query"]>;
stripe: T["stripe"];
paginate: boolean;
};
export interface BaseService<I extends IUnderScoreFunctions,
Find extends I["_find"] = I["_find"],
Get extends I["_get"] = I["_get"],
Create extends I["_create"] = I["_create"],
Update extends I["_update"] = I["_update"],
Patch extends I["_patch"] = I["_patch"],
Remove extends I["_remove"] = I["_remove"],
Search extends I["_search"] = I["_search"]
> {
_search?(params: Parameters<Search>[0]): ReturnType<Search>;
}
type Remove$QueryKeys<T> = T extends Array<infer ArrayItem> ? Remove$QueryKeys<ArrayItem>[] : T extends object ? {
[K in keyof T as string extends K ? never : K extends `$${infer DollarlessKey}` ? DollarlessKey : K]: T[K] extends object ? Remove$QueryKeys<T[K]> : T[K]
} : never;
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export abstract class BaseService<
I extends IUnderScoreFunctions,
Find extends I["_find"] = I["_find"],
Get extends I["_get"] = I["_get"],
Create extends I["_create"] = I["_create"],
Update extends I["_update"] = I["_update"],
Patch extends I["_patch"] = I["_patch"],
Remove extends I["_remove"] = I["_remove"],
Search extends I["_search"] = I["_search"]
> {
stripe: Stripe;
options: StripeServiceOptions;
abstract _find(params: Parameters<Find>[0]): ReturnType<Find>;
abstract _get(id: string, params: Parameters<Get>[1]): ReturnType<Get>;
abstract _create(
data: Parameters<Create>[0],
params: Parameters<Create>[1]
): ReturnType<Create>;
abstract _update(
id: string,
data: Parameters<Update>[1],
params: Parameters<Update>[2]
): ReturnType<Update>;
abstract _patch(
id: string,
data: Parameters<Patch>[1],
params: Parameters<Patch>[2]
): ReturnType<Patch>;
abstract _remove(
id: string,
params: Parameters<Remove>[1]
): ReturnType<Remove>;
constructor(options: StripeServiceOptions) {
const opts = {
// Stripe enforces 100 max and 10 default
paginate: {
default: 10,
max: 100
},
...options
};
if (!opts.stripe) {
throw new Error("Stripe service option `stripe` needs to be provided");
}
this.stripe = opts.stripe;
}
find(params: Parameters<Find>[0]): ReturnType<Find> {
if (!(this as any)._find) {
throw new NotImplemented("Find method not implemented");
}
return (this as any)._find(params).catch(this.handleError);
}
get(id: Parameters<Get>[0], params: Parameters<Get>[1]): ReturnType<Get> {
if (!(this as any)._get) {
throw new NotImplemented("Get method not implemented");
}
return (this as any)._get(id, params).catch(this.handleError);
}
create(
data: Parameters<Create>[0],
params: Parameters<Create>[1]
): ReturnType<Create> {
if (!(this as any)._create) {
throw new NotImplemented("Create method not implemented");
}
return (this as any)._create(data, params).catch(this.handleError);
}
update(
id: Parameters<Update>[0],
data: Parameters<Update>[1],
params: Parameters<Update>[2]
): ReturnType<Update> {
if (!(this as any)._update) {
throw new NotImplemented("Update method not implemented");
}
return (this as any)._update(id, data, params).catch(this.handleError);
}
patch(
id: Parameters<Patch>[0],
data: Parameters<Patch>[1],
params: Parameters<Patch>[2]
): ReturnType<Patch> {
if (!(this as any)._patch) {
throw new NotImplemented("Patch method not implemented");
}
return (this as any)._patch(id, data, params).catch(this.handleError);
}
remove(
id: Parameters<Remove>[0],
params: Parameters<Remove>[1]
): ReturnType<Remove> {
if (!(this as any)._remove) {
throw new NotImplemented("Remove method not implemented");
}
return (this as any)._remove(id, params).catch(this.handleError);
}
search(params: Parameters<Search>[0]): ReturnType<Search> {
if (!(this as any)._search) {
throw new NotImplemented("Search method not implemented");
}
return (this as any)._search(params).catch(this.handleError);
}
getLimit(
limit: number | undefined,
paramsPaginate: false | { max: number } | undefined
): number {
if (paramsPaginate === false) {
return limit;
}
const paginate = this.options?.paginate;
if (paginate && (paginate.default || paginate.max)) {
const base = paginate.default || 0;
const lower = typeof limit === "number" && !isNaN(limit) ? limit : base;
const upper =
typeof paginate.max === "number" ? paginate.max : Number.MAX_VALUE;
return Math.min(lower, upper);
}
return limit;
}
cleanQuery<Q extends Query | Query[]>(query: Q): Remove$QueryKeys<Q> {
if (Array.isArray(query)) {
// @ts-expect-error TODO: fix this
return query.map((item) => this.cleanQuery(item));
}
if (_.isObject(query)) {
const result = Object.assign({}, query);
Object.entries(result).forEach(([key, value]) => {
let cleanKey = key;
if (key.startsWith("$")) {
// @ts-expect-error TODO: fix this
delete result[key];
cleanKey = key.replace("$", "");
}
// @ts-expect-error TODO: fix this
result[cleanKey] = this.cleanQuery(value);
});
return result as unknown as Remove$QueryKeys<Q>;
}
return query as unknown as Remove$QueryKeys<Q>;
}
filterQuery<
P extends ParamsWithStripe,
Q = P extends Params<infer T> ? T : Query
>(params: P): Remove$QueryKeys<Q> {
const query = Object.assign({}, params.query);
const limit = query.$limit ?? query.limit;
if (limit) {
query.limit = this.getLimit(limit, params.paginate);
delete query.$limit;
}
return this.cleanQuery<Q>(query);
}
filterParams<T extends ParamsWithStripe = ParamsWithStripe>(
params: T
): FilteredParams<T> {
return {
query: this.filterQuery(params),
stripe: params.stripe,
paginate: params.paginate !== false
};
}
async handlePaginate<R = any, M extends Stripe.ApiListPromise<R> | Stripe.ApiSearchResultPromise<R> = Stripe.ApiListPromise<R>>(
{ paginate }: FilteredParams,
stripeMethod: M
): Promise<R[] | (M extends Stripe.ApiListPromise<R> ? Stripe.ApiList<R> : Stripe.ApiSearchResult<R>)> {
if (paginate) {
return (await stripeMethod) as (M extends Stripe.ApiListPromise<R> ? Stripe.ApiList<R> : Stripe.ApiSearchResult<R>);
}
if (stripeMethod.autoPagingEach) {
// NOTE: This is similar to stripe's autoPagingToArray
// but bypasses the 10,000 limit to better follow
// feathers pagination standards. You get better
// performance when using $limit because stripe
// will use Math.min($limit, 100) as the "chunk"
// size for each page of autoPagingEach. When
// no $limit is provided, it falls back to a
// page size of 10.
const results: R[] = [];
await stripeMethod.autoPagingEach((result) => {
results.push(result);
});
return results;
}
throw new MethodNotAllowed("Cannot use paginate: false on this method");
}
handleError(error: Stripe.errors.StripeError) {
if (!error.type) {
throw new GeneralError("Unknown Payment Gateway Error", error);
}
switch (error.type) {
case "StripeCardError":
// A declined card error
throw new PaymentError(error, error);
case "StripeInvalidRequestError":
// Invalid parameters were supplied to Stripe's API
throw new BadRequest(error, error);
case "StripeAPIError":
// An error occurred internally with Stripe's API
throw new Unavailable(error, error);
case "StripeConnectionError":
// Some kind of error occurred during the HTTPS communication
throw new Unavailable(error, error);
case "StripeAuthenticationError":
// You probably used an incorrect API key
throw new NotAuthenticated(error, error);
case "StripeRateLimitError":
// Too many requests
throw new TooManyRequests(error, error);
default:
throw new GeneralError("Unknown Payment Gateway Error", error);
}
}
}