349 lines
13 KiB
JavaScript
349 lines
13 KiB
JavaScript
// run in context allows us to change the templateSettings without causing havoc
|
|
const _ = require('lodash').runInContext();
|
|
const {lastPeriodStart, SUPPORTED_INTERVALS} = require('./date-utils');
|
|
|
|
_.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
|
|
|
|
class Limit {
|
|
/**
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} options.name - name of the limit
|
|
* @param {String} options.error - error message to use when limit is reached
|
|
* @param {String} options.helpLink - URL to the resource explaining how the limit works
|
|
* @param {Object} [options.db] - instance of knex db connection that currentCountQuery can use to run state check through
|
|
* @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors)
|
|
*/
|
|
constructor({name, error, helpLink, db, errors}) {
|
|
this.name = name;
|
|
this.error = error;
|
|
this.helpLink = helpLink;
|
|
this.db = db;
|
|
this.errors = errors;
|
|
}
|
|
|
|
generateError() {
|
|
let errorObj = {
|
|
errorDetails: {
|
|
name: this.name
|
|
}
|
|
};
|
|
|
|
if (this.helpLink) {
|
|
errorObj.help = this.helpLink;
|
|
}
|
|
|
|
return errorObj;
|
|
}
|
|
}
|
|
|
|
class MaxLimit extends Limit {
|
|
/**
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} options.name - name of the limit
|
|
* @param {Object} options.config - limit configuration
|
|
* @param {Number} options.config.max - maximum limit the limit would check against
|
|
* @param {Function} options.config.currentCountQuery - query checking the state that would be compared against the limit
|
|
* @param {Function} [options.config.formatter] - function to format the limit counts before they are passed to the error message
|
|
* @param {String} [options.config.error] - error message to use when limit is reached
|
|
* @param {String} [options.helpLink] - URL to the resource explaining how the limit works
|
|
* @param {Object} [options.db] - instance of knex db connection that currentCountQuery can use to run state check through
|
|
* @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors)
|
|
*/
|
|
constructor({name, config, helpLink, db, errors}) {
|
|
super({name, error: config.error || '', helpLink, db, errors});
|
|
|
|
if (config.max === undefined) {
|
|
throw new errors.IncorrectUsageError({message: 'Attempted to setup a max limit without a limit'});
|
|
}
|
|
|
|
if (!config.currentCountQuery) {
|
|
throw new errors.IncorrectUsageError({message: 'Attempted to setup a max limit without a current count query'});
|
|
}
|
|
|
|
this.currentCountQueryFn = config.currentCountQuery;
|
|
this.max = config.max;
|
|
this.formatter = config.formatter;
|
|
this.fallbackMessage = `This action would exceed the ${_.lowerCase(this.name)} limit on your current plan.`;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Number} count - current count that acceded the limit
|
|
* @returns {Object} instance of HostLimitError
|
|
*/
|
|
generateError(count) {
|
|
let errorObj = super.generateError();
|
|
|
|
errorObj.message = this.fallbackMessage;
|
|
|
|
if (this.error) {
|
|
const formatter = this.formatter || Intl.NumberFormat().format;
|
|
try {
|
|
errorObj.message = _.template(this.error)(
|
|
{
|
|
max: formatter(this.max),
|
|
count: formatter(count),
|
|
name: this.name
|
|
});
|
|
} catch (e) {
|
|
errorObj.message = this.fallbackMessage;
|
|
}
|
|
}
|
|
|
|
errorObj.errorDetails.limit = this.max;
|
|
errorObj.errorDetails.total = count;
|
|
|
|
return new this.errors.HostLimitError(errorObj);
|
|
}
|
|
|
|
async currentCountQuery() {
|
|
return await this.currentCountQueryFn(this.db);
|
|
}
|
|
|
|
/**
|
|
* Throws a HostLimitError if the configured or passed max limit is acceded by currentCountQuery
|
|
*
|
|
* @param {Object} options
|
|
* @param {Number} [options.max] - overrides configured default max value to perform checks against
|
|
* @param {Number} [options.addedCount] - number of items to add to the currentCount during the check
|
|
*/
|
|
async errorIfWouldGoOverLimit({max, addedCount = 1} = {}) {
|
|
let currentCount = await this.currentCountQuery();
|
|
|
|
if ((currentCount + addedCount) > (max || this.max)) {
|
|
throw this.generateError(currentCount);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws a HostLimitError if the configured or passed max limit is acceded by currentCountQuery
|
|
*
|
|
* @param {Object} options
|
|
* @param {Number} [options.max] - overrides configured default max value to perform checks against
|
|
* @param {Number} [options.currentCount] - overrides currentCountQuery to perform checks against
|
|
*/
|
|
async errorIfIsOverLimit({max, currentCount} = {}) {
|
|
currentCount = currentCount || await this.currentCountQuery();
|
|
|
|
if (currentCount > (max || this.max)) {
|
|
throw this.generateError(currentCount);
|
|
}
|
|
}
|
|
}
|
|
|
|
class MaxPeriodicLimit extends Limit {
|
|
/**
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} options.name - name of the limit
|
|
* @param {Object} options.config - limit configuration
|
|
* @param {Number} options.config.maxPeriodic - maximum limit the limit would check against
|
|
* @param {String} options.config.error - error message to use when limit is reached
|
|
* @param {Function} options.config.currentCountQuery - query checking the state that would be compared against the limit
|
|
* @param {('month')} options.config.interval - an interval to take into account when checking the limit. Currently only supports 'month' value
|
|
* @param {String} options.config.startDate - start date in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601), used to calculate period intervals
|
|
* @param {String} options.helpLink - URL to the resource explaining how the limit works
|
|
* @param {Object} [options.db] - instance of knex db connection that currentCountQuery can use to run state check through
|
|
* @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors)
|
|
*/
|
|
constructor({name, config, helpLink, db, errors}) {
|
|
super({name, error: config.error || '', helpLink, db, errors});
|
|
|
|
if (config.maxPeriodic === undefined) {
|
|
throw new errors.IncorrectUsageError({message: 'Attempted to setup a periodic max limit without a limit'});
|
|
}
|
|
|
|
if (!config.currentCountQuery) {
|
|
throw new errors.IncorrectUsageError({message: 'Attempted to setup a periodic max limit without a current count query'});
|
|
}
|
|
|
|
if (!config.interval) {
|
|
throw new errors.IncorrectUsageError({message: 'Attempted to setup a periodic max limit without an interval'});
|
|
}
|
|
|
|
if (!SUPPORTED_INTERVALS.includes(config.interval)) {
|
|
throw new errors.IncorrectUsageError({message: `Attempted to setup a periodic max limit without unsupported interval. Please specify one of: ${SUPPORTED_INTERVALS}`});
|
|
}
|
|
|
|
if (!config.startDate) {
|
|
throw new errors.IncorrectUsageError({message: 'Attempted to setup a periodic max limit without a start date'});
|
|
}
|
|
|
|
this.currentCountQueryFn = config.currentCountQuery;
|
|
this.maxPeriodic = config.maxPeriodic;
|
|
this.interval = config.interval;
|
|
this.startDate = config.startDate;
|
|
this.fallbackMessage = `This action would exceed the ${_.lowerCase(this.name)} limit on your current plan.`;
|
|
}
|
|
|
|
generateError(count) {
|
|
let errorObj = super.generateError();
|
|
|
|
errorObj.message = this.fallbackMessage;
|
|
|
|
if (this.error) {
|
|
try {
|
|
errorObj.message = _.template(this.error)(
|
|
{
|
|
max: Intl.NumberFormat().format(this.maxPeriodic),
|
|
count: Intl.NumberFormat().format(count),
|
|
name: this.name
|
|
});
|
|
} catch (e) {
|
|
errorObj.message = this.fallbackMessage;
|
|
}
|
|
}
|
|
|
|
errorObj.errorDetails.limit = this.maxPeriodic;
|
|
errorObj.errorDetails.total = count;
|
|
|
|
return new this.errors.HostLimitError(errorObj);
|
|
}
|
|
|
|
async currentCountQuery() {
|
|
const lastPeriodStartDate = lastPeriodStart(this.startDate, this.interval);
|
|
|
|
return await this.currentCountQueryFn(this.db, lastPeriodStartDate);
|
|
}
|
|
|
|
/**
|
|
* Throws a HostLimitError if the configured or passed max limit is acceded by currentCountQuery
|
|
*
|
|
* @param {Object} options
|
|
* @param {Number} [options.max] - overrides configured default maxPeriodic value to perform checks against
|
|
* @param {Number} [options.addedCount] - number of items to add to the currentCount during the check
|
|
*/
|
|
async errorIfWouldGoOverLimit({max, addedCount = 1} = {}) {
|
|
let currentCount = await this.currentCountQuery();
|
|
|
|
if ((currentCount + addedCount) > (max || this.maxPeriodic)) {
|
|
throw this.generateError(currentCount);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws a HostLimitError if the configured or passed max limit is acceded by currentCountQuery
|
|
*
|
|
* @param {Object} options
|
|
* @param {Number} [options.max] - overrides configured default maxPeriodic value to perform checks against
|
|
*/
|
|
async errorIfIsOverLimit({max} = {}) {
|
|
let currentCount = await this.currentCountQuery();
|
|
|
|
if (currentCount > (max || this.maxPeriodic)) {
|
|
throw this.generateError(currentCount);
|
|
}
|
|
}
|
|
}
|
|
|
|
class FlagLimit extends Limit {
|
|
/**
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} options.name - name of the limit
|
|
* @param {Object} options.config - limit configuration
|
|
* @param {Number} options.config.disabled - disabled/enabled flag for the limit
|
|
* @param {String} options.config.error - error message to use when limit is reached
|
|
* @param {String} options.helpLink - URL to the resource explaining how the limit works
|
|
* @param {Object} [options.db] - instance of knex db connection that currentCountQuery can use to run state check through
|
|
* @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors)
|
|
*/
|
|
constructor({name, config, helpLink, db, errors}) {
|
|
super({name, error: config.error || '', helpLink, db, errors});
|
|
|
|
this.disabled = config.disabled;
|
|
this.fallbackMessage = `Your plan does not support ${_.lowerCase(this.name)}. Please upgrade to enable ${_.lowerCase(this.name)}.`;
|
|
}
|
|
|
|
generateError() {
|
|
let errorObj = super.generateError();
|
|
|
|
if (this.error) {
|
|
errorObj.message = this.error;
|
|
} else {
|
|
errorObj.message = this.fallbackMessage;
|
|
}
|
|
|
|
return new this.errors.HostLimitError(errorObj);
|
|
}
|
|
|
|
/**
|
|
* Flag limits are on/off so using a feature is always over the limit
|
|
*/
|
|
async errorIfWouldGoOverLimit() {
|
|
if (this.disabled) {
|
|
throw this.generateError();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flag limits are on/off. They don't necessarily mean the limit wasn't possible to reach
|
|
* NOTE: this method should not be relied on as it's impossible to check the limit was surpassed!
|
|
*/
|
|
async errorIfIsOverLimit() {
|
|
return;
|
|
}
|
|
}
|
|
|
|
class AllowlistLimit extends Limit {
|
|
/**
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} options.name - name of the limit
|
|
* @param {Object} options.config - limit configuration
|
|
* @param {[String]} options.config.allowlist - allowlist values that would be compared against
|
|
* @param {String} options.config.error - error message to use when limit is reached
|
|
* @param {String} options.helpLink - URL to the resource explaining how the limit works
|
|
* @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors)
|
|
*/
|
|
constructor({name, config, helpLink, errors}) {
|
|
super({name, error: config.error || '', helpLink, errors});
|
|
|
|
if (!config.allowlist || !config.allowlist.length) {
|
|
throw new this.errors.IncorrectUsageError({message: 'Attempted to setup an allowlist limit without an allowlist'});
|
|
}
|
|
|
|
this.allowlist = config.allowlist;
|
|
this.fallbackMessage = `This action would exceed the ${_.lowerCase(this.name)} limit on your current plan.`;
|
|
}
|
|
|
|
generateError() {
|
|
let errorObj = super.generateError();
|
|
|
|
if (this.error) {
|
|
errorObj.message = this.error;
|
|
} else {
|
|
errorObj.message = this.fallbackMessage;
|
|
}
|
|
|
|
return new this.errors.HostLimitError(errorObj);
|
|
}
|
|
|
|
async errorIfWouldGoOverLimit(metadata) {
|
|
if (!metadata || !metadata.value) {
|
|
throw new this.errors.IncorrectUsageError({message: 'Attempted to check an allowlist limit without a value'});
|
|
}
|
|
if (!this.allowlist.includes(metadata.value)) {
|
|
throw this.generateError();
|
|
}
|
|
}
|
|
|
|
async errorIfIsOverLimit(metadata) {
|
|
if (!metadata || !metadata.value) {
|
|
throw new this.errors.IncorrectUsageError({message: 'Attempted to check an allowlist limit without a value'});
|
|
}
|
|
if (!this.allowlist.includes(metadata.value)) {
|
|
throw this.generateError();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
MaxLimit,
|
|
MaxPeriodicLimit,
|
|
FlagLimit,
|
|
AllowlistLimit
|
|
};
|