@akinolae/node-rate-limiter
Version:
216 lines (187 loc) • 5.03 kB
text/typescript
import { Request } from 'express';
enum Intervals {
// Query every 1 minute
Query = 1 * 60 * 1000,
request = 10,
}
type RequestStore = {
callID: string;
request: number;
ttl: number;
lastCall: number;
requestIp: string;
};
type Requestparams = {
ttl?: number;
session_no?: number;
request: Request;
};
type RequestStoreObject = {
[key: string]: Array<RequestStore>;
};
let requestBucket: RequestStoreObject;
/**
* @param {*} id
* @returns a single call that matches the provided ID
* checks if the call id exists in the interval-list
*/
const regexCheck = (defaultVal: string, val: string) =>
new RegExp(defaultVal).test(val);
const findRequestbyId = (params: {
requestIp: string;
functionId: string;
}): RequestStore | undefined => {
const { requestIp, functionId } = params;
if (!requestIp || !functionId) {
throw new Error('INTERVAL_CACHE_ERROR:CallId and functionId is required');
}
const requestFromBucket =
requestBucket !== undefined ? requestBucket[requestIp] : [];
if (Array.isArray(requestFromBucket) && requestFromBucket.length > 0) {
for (const interval of requestFromBucket) {
if (regexCheck(interval.callID, functionId)) {
return { ...interval, requestIp };
} else {
return;
}
}
}
};
/**
* @param {IntervalObj} params
* filters the call list array, removes duplicate before adding a new call object
* checks if the call id exists in the interval-list
*/
const addRequestToList = (params: {
requestIp: string;
requestObject: RequestStore;
}) => {
const { requestIp, requestObject, ...rest } = params;
// Check that the callid exists before performing any mutation
const callidExists = Object.keys(requestBucket || {}).some(
(id) => id === requestIp,
);
if (!callidExists) {
requestBucket = {
...requestBucket,
...rest,
[requestIp]: [requestObject],
};
} else {
const currentInterval = [
...requestBucket[requestIp].filter(
(a) => a?.callID !== requestObject.callID,
),
requestObject,
];
requestBucket = {
...requestBucket,
[requestIp]: currentInterval,
};
}
return requestBucket;
};
/**
*
* @param params
* @returns
* manages the request coming in, checks that each request updates ones
*/
const manageRequestFn = (params: {
callIDexists: RequestStore;
currentRequestId: string;
urlFn: string;
session: number;
interval: number;
}) => {
const { callIDexists, currentRequestId, urlFn, interval, session } = params;
let requestBody: any;
const date = new Date();
//add fn() to scope if it doesn't exist
if (!callIDexists || !Object.keys(callIDexists).length) {
requestBody = {
requestIp: currentRequestId,
requestObject: {
callID: urlFn,
request: session,
ttl: interval,
lastCall: +date,
},
};
} else if (
callIDexists.request <= 1 &&
date.getTime() - callIDexists.lastCall > interval
) {
requestBody = {
requestIp: callIDexists.requestIp,
requestObject: {
callID: callIDexists.callID,
request: session,
ttl: callIDexists.ttl,
lastCall: +date,
},
};
} else {
if (!callIDexists.request) {
throw new Error(
`TOO MANY REQUESTS: call ${urlFn} has too many requests try again in ${Math.floor(
callIDexists.ttl / 60000,
)} minute(s)`,
);
} else {
const newRequestLimit = callIDexists.request - 1;
requestBody = {
requestIp: callIDexists.requestIp,
requestObject: {
callID: callIDexists.callID,
request: newRequestLimit,
ttl: callIDexists.ttl,
lastCall: callIDexists.lastCall,
},
};
}
}
return requestBody;
};
/**
* @param {*} params
* does the necessary checks and also updates the state of each request
*/
const limitCorefn = (args: Requestparams) => {
/**
* Checks to know if fn by callId exists in scope
* Adds call to the scope if it doesn't exist
*/
let urlFn: string;
const validUrl = ['graphql'];
const { session_no, ttl, request } = args;
const reqIsGraphql = validUrl.some((url: string) =>
regexCheck(url, request.baseUrl),
);
if (reqIsGraphql) {
const _req = Object.keys(request.body);
if (
_req.some((val: string) => ['query'].includes(val.toLocaleLowerCase()))
) {
urlFn = request.body[_req[0]].trimStart().split(' ')[1].split('(')[0];
}
} else {
urlFn = request.url;
}
const interval = ttl ?? Intervals.Query;
const session = session_no ?? Intervals.request;
const currentRequestId = `${request.ip}-${urlFn}`;
const callIDexists = findRequestbyId({
requestIp: currentRequestId,
functionId: urlFn,
});
const requestBody = manageRequestFn({
callIDexists,
currentRequestId,
interval,
session,
urlFn,
});
addRequestToList(requestBody);
};
export { limitCorefn as requestLimitCore };