2021-03-03 20:50:17 +03:00
// run in context allows us to change the templateSettings without causing havoc
const _ = require ( 'lodash' ) . runInContext ( ) ;
2021-05-06 16:48:31 +03:00
const { lastPeriodStart , SUPPORTED _INTERVALS } = require ( './date-utils' ) ;
2021-05-06 13:16:22 +03:00
2021-03-03 15:18:51 +03:00
_ . templateSettings . interpolate = /{{([\s\S]+?)}}/g ;
class Limit {
2021-04-05 07:17:57 +03:00
constructor ( { name , error , helpLink , db , errors } ) {
2021-03-03 15:18:51 +03:00
this . name = name ;
this . error = error ;
this . helpLink = helpLink ;
this . db = db ;
2021-04-05 07:17:57 +03:00
this . errors = errors ;
2021-03-03 15:18:51 +03:00
}
generateError ( ) {
let errorObj = {
errorDetails : {
name : this . name
}
} ;
if ( this . helpLink ) {
errorObj . help = this . helpLink ;
}
return errorObj ;
}
}
class MaxLimit extends Limit {
2021-04-01 07:29:26 +03:00
/ * *
*
* @ 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 { 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
2021-04-05 07:17:57 +03:00
* @ param { Object } options . errors - instance of errors compatible with Ghost - Ignition ' s errors ( https : //github.com/TryGhost/Ignition#errors)
2021-04-01 07:29:26 +03:00
* /
2021-04-05 07:17:57 +03:00
constructor ( { name , config , helpLink , db , errors } ) {
super ( { name , error : config . error || '' , helpLink , db , errors } ) ;
2021-03-03 15:18:51 +03:00
2021-04-01 07:20:56 +03:00
if ( config . max === undefined ) {
2021-05-06 14:42:02 +03:00
throw new errors . IncorrectUsageError ( { message : 'Attempted to setup a max limit without a limit' } ) ;
2021-04-01 07:20:56 +03:00
}
2021-03-03 15:18:51 +03:00
if ( ! config . currentCountQuery ) {
2021-05-06 14:42:02 +03:00
throw new errors . IncorrectUsageError ( { message : 'Attempted to setup a max limit without a current count query' } ) ;
2021-03-03 15:18:51 +03:00
}
this . currentCountQueryFn = config . currentCountQuery ;
this . max = config . max ;
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 {
2021-03-04 21:08:25 +03:00
errorObj . message = _ . template ( this . error ) (
{
max : Intl . NumberFormat ( ) . format ( this . max ) ,
2021-05-12 13:43:34 +03:00
count : Intl . NumberFormat ( ) . format ( count ) ,
name : this . name
2021-03-04 21:08:25 +03:00
} ) ;
2021-03-03 15:18:51 +03:00
} catch ( e ) {
errorObj . message = this . fallbackMessage ;
}
}
2021-03-04 21:08:25 +03:00
errorObj . errorDetails . limit = this . max ;
2021-03-03 15:18:51 +03:00
errorObj . errorDetails . total = count ;
2021-04-05 07:17:57 +03:00
return new this . errors . HostLimitError ( errorObj ) ;
2021-03-03 15:18:51 +03:00
}
async currentCountQuery ( ) {
return await this . currentCountQueryFn ( this . db ) ;
}
2021-04-01 07:59:52 +03:00
/ * *
* Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery
*
* @ param { Object } options
* @ param { Number } [ options . max ] - overrides configured default max value to perform checks against
* /
2021-05-07 17:13:01 +03:00
async errorIfWouldGoOverLimit ( { max , addedCount = 1 } = { } ) {
2021-03-03 15:18:51 +03:00
let currentCount = await this . currentCountQuery ( this . db ) ;
2021-04-01 07:59:52 +03:00
2021-05-07 17:13:01 +03:00
if ( ( currentCount + addedCount ) > ( max || this . max ) ) {
2021-03-03 15:18:51 +03:00
throw this . generateError ( currentCount ) ;
}
}
2021-04-01 07:59:52 +03:00
2021-04-01 08:27:29 +03:00
/ * *
* Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery
*
* @ param { Object } options
* @ param { Number } [ options . max ] - overrides configured default max value to perform checks against
* /
async errorIfIsOverLimit ( { max } = { } ) {
2021-03-03 15:18:51 +03:00
let currentCount = await this . currentCountQuery ( this . db ) ;
2021-04-01 08:27:29 +03:00
if ( currentCount > ( max || this . max ) ) {
2021-03-03 15:18:51 +03:00
throw this . generateError ( currentCount ) ;
}
}
}
2021-05-06 13:16:22 +03:00
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 { 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 Ghost - Ignition ' s errors ( https : //github.com/TryGhost/Ignition#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. ` ;
}
2021-05-06 16:48:31 +03:00
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 ) ,
2021-05-12 13:43:34 +03:00
count : Intl . NumberFormat ( ) . format ( count ) ,
name : this . name
2021-05-06 16:48:31 +03:00
} ) ;
} 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 ecceded by currentCountQuery
*
* @ param { Object } options
* @ param { Number } [ options . max ] - overrides configured default maxPeriodic value to perform checks against
* /
2021-05-07 17:13:01 +03:00
async errorIfWouldGoOverLimit ( { max , addedCount = 1 } = { } ) {
2021-05-06 16:48:31 +03:00
let currentCount = await this . currentCountQuery ( this . db ) ;
2021-05-07 17:13:01 +03:00
if ( ( currentCount + addedCount ) > ( max || this . maxPeriodic ) ) {
2021-05-06 16:48:31 +03:00
throw this . generateError ( currentCount ) ;
}
}
/ * *
* Throws a HostLimitError if the configured or passed max limit is ecceded 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 ( this . db ) ;
if ( currentCount > ( max || this . maxPeriodic ) ) {
throw this . generateError ( currentCount ) ;
}
}
2021-05-06 13:16:22 +03:00
}
2021-03-03 15:18:51 +03:00
class FlagLimit extends Limit {
2021-04-05 07:03:36 +03:00
/ * *
*
* @ 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 . 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
2021-04-05 07:17:57 +03:00
* @ param { Object } options . errors - instance of errors compatible with Ghost - Ignition ' s errors ( https : //github.com/TryGhost/Ignition#errors)
2021-04-05 07:03:36 +03:00
* /
2021-04-05 07:17:57 +03:00
constructor ( { name , config , helpLink , db , errors } ) {
super ( { name , error : config . error || '' , helpLink , db , errors } ) ;
2021-03-03 15:18:51 +03:00
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 ;
}
2021-04-05 07:17:57 +03:00
return new this . errors . HostLimitError ( errorObj ) ;
2021-03-03 15:18:51 +03:00
}
/ * *
* Flag limits are on / off so using a feature is always over the limit
* /
async errorIfWouldGoOverLimit ( ) {
if ( this . disabled ) {
throw this . generateError ( ) ;
}
}
/ * *
2021-04-09 07:10:14 +03:00
* 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 !
2021-03-03 15:18:51 +03:00
* /
async errorIfIsOverLimit ( ) {
return ;
}
}
2021-04-08 18:29:53 +03:00
class AllowlistLimit extends Limit {
constructor ( { name , config , helpLink , errors } ) {
super ( { name , error : config . error || '' , helpLink , errors } ) ;
if ( ! config . allowlist || ! config . allowlist . length ) {
2021-05-06 14:42:02 +03:00
throw new this . errors . IncorrectUsageError ( { message : 'Attempted to setup an allowlist limit without an allowlist' } ) ;
2021-04-08 18:29:53 +03:00
}
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 ) {
2021-05-21 13:09:27 +03:00
if ( ! metadata || ! metadata . value ) {
2021-05-06 14:42:02 +03:00
throw new this . errors . IncorrectUsageError ( { message : 'Attempted to check an allowlist limit without a value' } ) ;
2021-04-08 18:29:53 +03:00
}
if ( ! this . allowlist . includes ( metadata . value ) ) {
throw this . generateError ( ) ;
}
}
async errorIfIsOverLimit ( metadata ) {
2021-05-21 13:09:27 +03:00
if ( ! metadata || ! metadata . value ) {
2021-05-06 14:42:02 +03:00
throw new this . errors . IncorrectUsageError ( { message : 'Attempted to check an allowlist limit without a value' } ) ;
2021-04-08 18:29:53 +03:00
}
if ( ! this . allowlist . includes ( metadata . value ) ) {
throw this . generateError ( ) ;
}
}
}
2021-03-03 15:18:51 +03:00
module . exports = {
MaxLimit ,
2021-05-06 13:16:22 +03:00
MaxPeriodicLimit ,
2021-04-08 18:29:53 +03:00
FlagLimit ,
AllowlistLimit
2021-03-03 15:18:51 +03:00
} ;