From b9058f6b38fc33531929432d4e20b53f0aaec04d Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 3 Mar 2021 12:18:51 +0000 Subject: [PATCH 001/255] Added limit service initial commit - This provides some basic functionality and error message generation for adding host-based limits in Ghost - It is a first-pass, needs unit tests etc --- .eslintrc.js | 6 ++ LICENSE | 21 +++++++ README.md | 39 +++++++++++++ index.js | 0 lib/config.js | 26 +++++++++ lib/limit-service.js | 76 +++++++++++++++++++++++++ lib/limit.js | 120 +++++++++++++++++++++++++++++++++++++++ package.json | 30 ++++++++++ test/.eslintrc.js | 6 ++ test/hello.test.js | 10 ++++ test/utils/assertions.js | 11 ++++ test/utils/index.js | 11 ++++ test/utils/overrides.js | 10 ++++ 13 files changed, 366 insertions(+) create mode 100644 .eslintrc.js create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/config.js create mode 100644 lib/limit-service.js create mode 100644 lib/limit.js create mode 100644 package.json create mode 100644 test/.eslintrc.js create mode 100644 test/hello.test.js create mode 100644 test/utils/assertions.js create mode 100644 test/utils/index.js create mode 100644 test/utils/overrides.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000..c9c1bcb5226 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..366ae5f6246 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2021 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000000..d2a2acae7c2 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Limit Service + +## Install + +`npm install @tryghost/limit-service --save` + +or + +`yarn add @tryghost/limit-service` + + +## Usage + + +## Develop + +This is a mono repository, managed with [lerna](https://lernajs.io/). + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `yarn` to install top-level dependencies. + + +## Run + +- `yarn dev` + + +## Test + +- `yarn lint` run just eslint +- `yarn test` run lint and tests + + + + +# Copyright & License + +Copyright (c) 2013-2021 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 00000000000..1835441a407 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,26 @@ +module.exports = { + members: { + currentCountQuery: async (db) => { + let result = await db.knex('members').count('id', {as: 'count'}).first(); + return result.count; + } + }, + staff: { + currentCountQuery: async (db) => { + let result = await db.knex('users') + .count('users.id', {as: 'count'}) + .leftJoin('roles_users', 'users.id', 'roles_users.user_id') + .leftJoin('roles', 'roles_users.role_id', 'roles.id') + .whereNot('roles.name', 'Contributor').andWhereNot('users.status', 'inactive').first(); + + return result.count; + } + }, + custom_integrations: { + currentCountQuery: async (db) => { + let result = await db.knex('integrations').count('id', {as: 'count'}).whereNotIn('type', ['internal', 'builtin']).first(); + return result.count; + } + }, + custom_themes: {} +}; diff --git a/lib/limit-service.js b/lib/limit-service.js new file mode 100644 index 00000000000..e2f3f355947 --- /dev/null +++ b/lib/limit-service.js @@ -0,0 +1,76 @@ +const errors = require('@tryghost/errors'); +const {MaxLimit, FlagLimit} = require('./limit'); +const config = require('./config'); +const _ = require('lodash'); + +class LimitService { + constructor() { + this.limits = {}; + } + + loadLimits({limits, helpLink, db}) { + Object.keys(limits).forEach((name) => { + if (config[name]) { + let limitConfig = _.merge({}, limits[name], config[name]); + + if (_.has(limitConfig, 'max')) { + this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db}); + } else { + this.limits[name] = new FlagLimit({name: name, config: limitConfig, helpLink}); + } + } + }); + } + + isLimited(limitName) { + return !!this.limits[limitName]; + } + + async checkIsOverLimit(limitName) { + if (!this.isLimited(limitName)) { + return; + } + + try { + await this.limits[limitName].errorIfIsOverLimit(); + return false; + } catch (error) { + if (error instanceof errors.HostLimitError) { + return true; + } + } + } + + async checkWouldGoOverLimit(limitName) { + if (!this.isLimited(limitName)) { + return; + } + + try { + await this.limits[limitName].errorIfWouldGoOverLimit(); + return false; + } catch (error) { + if (error instanceof errors.HostLimitError) { + return true; + } + } + } + + async errorIfIsOverLimit(limitName) { + if (!this.isLimited(limitName)) { + return; + } + + await this.limits[limitName].errorIfIsOverLimit(); + } + + async errorIfWouldGoOverLimit(limitName) { + if (!this.isLimited(limitName)) { + return; + } + + await this.limits[limitName].errorIfWouldGoOverLimit(); + } +} + +module.exports = LimitService; diff --git a/lib/limit.js b/lib/limit.js new file mode 100644 index 00000000000..cab8ff23ea2 --- /dev/null +++ b/lib/limit.js @@ -0,0 +1,120 @@ +const errors = require('@tryghost/errors'); +const _ = require('lodash'); + +_.templateSettings.interpolate = /{{([\s\S]+?)}}/g; + +class Limit { + constructor({name, error, helpLink, db}) { + this.name = name; + this.error = error; + this.helpLink = helpLink; + this.db = db; + } + + generateError() { + let errorObj = { + errorDetails: { + name: this.name + } + }; + + if (this.helpLink) { + errorObj.help = this.helpLink; + } + + return errorObj; + } +} + +class MaxLimit extends Limit { + constructor({name, config, helpLink, db}) { + super({name, error: config.error || '', helpLink, db}); + + if (!config.currentCountQuery) { + throw new errors.IncorrectUsageError('Attempted to setup a max limit without a current count query'); + } + + 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(); + let max = this.max; + + errorObj.message = this.fallbackMessage; + + if (this.error) { + try { + errorObj.message = _.template(this.error)({max, count}); + } catch (e) { + errorObj.message = this.fallbackMessage; + } + } + + errorObj.errorDetails.limit = max; + errorObj.errorDetails.total = count; + + return new errors.HostLimitError(errorObj); + } + + async currentCountQuery() { + return await this.currentCountQueryFn(this.db); + } + + async errorIfWouldGoOverLimit() { + let currentCount = await this.currentCountQuery(this.db); + if ((currentCount + 1) > this.max) { + throw this.generateError(currentCount); + } + } + async errorIfIsOverLimit() { + let currentCount = await this.currentCountQuery(this.db); + if (currentCount > this.max) { + throw this.generateError(currentCount); + } + } +} + +class FlagLimit extends Limit { + constructor({name, config, helpLink, db}) { + super({name, error: config.error || '', helpLink, db}); + + 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 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 so we can't be over the limit + */ + async errorIfIsOverLimit() { + return; + } +} + +module.exports = { + MaxLimit, + FlagLimit +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000000..5b27b820ee0 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tryghost/limit-service", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", + "author": "Ghost Foundation", + "license": "MIT", + "main": "./lib/limit-service.js", + "exports": "./lib/limit-service.js", + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing mocha './test/**/*.test.js'", + "lint": "eslint . --ext .js --cache", + "posttest": "yarn lint" + }, + "files": [ + "index.js", + "lib" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "mocha": "8.3.0", + "should": "13.2.3", + "sinon": "9.2.4" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 00000000000..829b601eb0a --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/test/hello.test.js b/test/hello.test.js new file mode 100644 index 00000000000..85d69d1e08c --- /dev/null +++ b/test/hello.test.js @@ -0,0 +1,10 @@ +// Switch these lines once there are useful utils +// const testUtils = require('./utils'); +require('./utils'); + +describe('Hello world', function () { + it('Runs a test', function () { + // TODO: Write me! + 'hello'.should.eql('hello'); + }); +}); diff --git a/test/utils/assertions.js b/test/utils/assertions.js new file mode 100644 index 00000000000..7364ee8aa19 --- /dev/null +++ b/test/utils/assertions.js @@ -0,0 +1,11 @@ +/** + * Custom Should Assertions + * + * Add any custom assertions to this file. + */ + +// Example Assertion +// should.Assertion.add('ExampleAssertion', function () { +// this.params = {operator: 'to be a valid Example Assertion'}; +// this.obj.should.be.an.Object; +// }); diff --git a/test/utils/index.js b/test/utils/index.js new file mode 100644 index 00000000000..0d67d86ff86 --- /dev/null +++ b/test/utils/index.js @@ -0,0 +1,11 @@ +/** + * Test Utilities + * + * Shared utils for writing tests + */ + +// Require overrides - these add globals for tests +require('./overrides'); + +// Require assertions - adds custom should assertions +require('./assertions'); diff --git a/test/utils/overrides.js b/test/utils/overrides.js new file mode 100644 index 00000000000..90203424ee2 --- /dev/null +++ b/test/utils/overrides.js @@ -0,0 +1,10 @@ +// This file is required before any test is run + +// Taken from the should wiki, this is how to make should global +// Should is a global in our eslint test config +global.should = require('should').noConflict(); +should.extend(); + +// Sinon is a simple case +// Sinon is a global in our eslint test config +global.sinon = require('sinon'); From eaa6f1ea19d89de172979cffb583e6c1ca1404c8 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 3 Mar 2021 12:20:40 +0000 Subject: [PATCH 002/255] Published new versions - @tryghost/limit-service@0.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b27b820ee0..5b476ac04f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.0.0", + "version": "0.1.0", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From f3adec35db7dbd18080c4258f64bc7a8dc8268ff Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 3 Mar 2021 17:50:17 +0000 Subject: [PATCH 003/255] Fixed clobbering the lodash template settings refs: https://github.com/lodash/lodash/issues/705 - Was seeing unexpected token = errors when using lodash templates in Ghost - This is because we're setting template settings globally in this dependency and it affects every other user of lodash - Using runInContext keeps this templateSettings change local to this lib - Test proves that after requiring limits we can require lodash and have the default values again --- lib/limit.js | 3 ++- test/hello.test.js | 10 ---------- test/template.test.js | 12 ++++++++++++ 3 files changed, 14 insertions(+), 11 deletions(-) delete mode 100644 test/hello.test.js create mode 100644 test/template.test.js diff --git a/lib/limit.js b/lib/limit.js index cab8ff23ea2..6edab24f1fe 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -1,6 +1,7 @@ const errors = require('@tryghost/errors'); -const _ = require('lodash'); +// run in context allows us to change the templateSettings without causing havoc +const _ = require('lodash').runInContext(); _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; class Limit { diff --git a/test/hello.test.js b/test/hello.test.js deleted file mode 100644 index 85d69d1e08c..00000000000 --- a/test/hello.test.js +++ /dev/null @@ -1,10 +0,0 @@ -// Switch these lines once there are useful utils -// const testUtils = require('./utils'); -require('./utils'); - -describe('Hello world', function () { - it('Runs a test', function () { - // TODO: Write me! - 'hello'.should.eql('hello'); - }); -}); diff --git a/test/template.test.js b/test/template.test.js new file mode 100644 index 00000000000..9af16229701 --- /dev/null +++ b/test/template.test.js @@ -0,0 +1,12 @@ +// Switch these lines once there are useful utils +// const testUtils = require('./utils'); +require('./utils'); + +describe('Lodash Template', function () { + it('Does not get clobbered by this lib', function () { + require('../lib/limit'); + let _ = require('lodash'); + + _.templateSettings.interpolate.should.eql(/<%=([\s\S]+?)%>/g); + }); +}); From 5dab1d225ab36a479d384551ae7a5f24299e202b Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 3 Mar 2021 17:56:02 +0000 Subject: [PATCH 004/255] Published new versions - @tryghost/limit-service@0.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b476ac04f7..d92f45a64ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.1.0", + "version": "0.1.1", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From f62fc5f9006ed05157e163f8f833b57e94f64130 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 4 Mar 2021 13:31:41 +0000 Subject: [PATCH 005/255] Updated staff count query to include invites refs: https://github.com/TryGhost/Team/issues/510 - we need to make sure we take into account any invites that could be accepted at any time - this counts all invites for non-contributor roles as well as all users who aren't contributors - this should stop there being loop holes to inviting staff users --- lib/config.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/config.js b/lib/config.js index 1835441a407..66c1e9e75a5 100644 --- a/lib/config.js +++ b/lib/config.js @@ -8,12 +8,17 @@ module.exports = { staff: { currentCountQuery: async (db) => { let result = await db.knex('users') - .count('users.id', {as: 'count'}) + .select('users.id') .leftJoin('roles_users', 'users.id', 'roles_users.user_id') .leftJoin('roles', 'roles_users.role_id', 'roles.id') - .whereNot('roles.name', 'Contributor').andWhereNot('users.status', 'inactive').first(); + .whereNot('roles.name', 'Contributor').andWhereNot('users.status', 'inactive').union([ + db.knex('invites') + .select('invites.id') + .leftJoin('roles', 'invites.role_id', 'roles.id') + .whereNot('roles.name', 'Contributor') + ]); - return result.count; + return result.length; } }, custom_integrations: { From 01107698c95512087e8d5907ddc04699fae9319d Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 4 Mar 2021 13:35:20 +0000 Subject: [PATCH 006/255] Published new versions - @tryghost/limit-service@0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d92f45a64ca..e7efbdc92f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.1.1", + "version": "0.2.0", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 118a20c82b78729ff9835eda49cc99e7d9a2b55f Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 4 Mar 2021 18:08:25 +0000 Subject: [PATCH 007/255] Added proper number formatting for error messages refs: https://github.com/TryGhost/Team/issues/510 - We should always format numbers correctly with thousand separators when we're displaying them to users --- lib/limit.js | 9 ++++++--- test/limit-service.test.js | 28 ++++++++++++++++++++++++++++ test/template.test.js | 12 ------------ 3 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 test/limit-service.test.js delete mode 100644 test/template.test.js diff --git a/lib/limit.js b/lib/limit.js index 6edab24f1fe..5be33cf4fba 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -42,19 +42,22 @@ class MaxLimit extends Limit { generateError(count) { let errorObj = super.generateError(); - let max = this.max; errorObj.message = this.fallbackMessage; if (this.error) { try { - errorObj.message = _.template(this.error)({max, count}); + errorObj.message = _.template(this.error)( + { + max: Intl.NumberFormat().format(this.max), + count: Intl.NumberFormat().format(count) + }); } catch (e) { errorObj.message = this.fallbackMessage; } } - errorObj.errorDetails.limit = max; + errorObj.errorDetails.limit = this.max; errorObj.errorDetails.total = count; return new errors.HostLimitError(errorObj); diff --git a/test/limit-service.test.js b/test/limit-service.test.js new file mode 100644 index 00000000000..19c1031dd44 --- /dev/null +++ b/test/limit-service.test.js @@ -0,0 +1,28 @@ +// Switch these lines once there are useful utils +// const testUtils = require('./utils'); +require('./utils'); + +describe('Limit Service', function () { + describe('Lodash Template', function () { + it('Does not get clobbered by this lib', function () { + require('../lib/limit'); + let _ = require('lodash'); + + _.templateSettings.interpolate.should.eql(/<%=([\s\S]+?)%>/g); + }); + }); + + describe('Error Messages', function () { + it('Formats numbers correctly', function () { + const {MaxLimit} = require('../lib/limit'); + + let limit = new MaxLimit({name: 'test', config: {max: 35000000, currentCountQuery: () => {}, error: 'Your plan supports up to {{max}} staff users. Please upgrade to add more.'}}); + + let error = limit.generateError(35000001); + + error.message.should.eql('Your plan supports up to 35,000,000 staff users. Please upgrade to add more.'); + error.errorDetails.limit.should.eql(35000000); + error.errorDetails.total.should.eql(35000001); + }); + }); +}); diff --git a/test/template.test.js b/test/template.test.js deleted file mode 100644 index 9af16229701..00000000000 --- a/test/template.test.js +++ /dev/null @@ -1,12 +0,0 @@ -// Switch these lines once there are useful utils -// const testUtils = require('./utils'); -require('./utils'); - -describe('Lodash Template', function () { - it('Does not get clobbered by this lib', function () { - require('../lib/limit'); - let _ = require('lodash'); - - _.templateSettings.interpolate.should.eql(/<%=([\s\S]+?)%>/g); - }); -}); From 10558c0ddcbb43997e2be23f5b73dffc79ce06e1 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 4 Mar 2021 18:15:13 +0000 Subject: [PATCH 008/255] Published new versions - @tryghost/limit-service@0.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7efbdc92f8..90250d7591d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.2.0", + "version": "0.2.1", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 05d28f05d0b1dc89f42932b20f13783ad5c8f6f7 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 4 Mar 2021 20:11:54 +0000 Subject: [PATCH 009/255] Changed casing of limit names + fixed handling refs: https://github.com/TryGhost/Team/issues/510 - Ghost config always uses camelcase. This was incorrectly implemented with snake case originally - Swap to use camelCase by default, which is desirable, but support both - It's really easy to support both in the loader and isLimited check, so we do this to stop ourselves tripping on this later --- lib/config.js | 4 +-- lib/limit-service.js | 4 ++- test/limit-service.test.js | 64 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/lib/config.js b/lib/config.js index 66c1e9e75a5..4c37d967f33 100644 --- a/lib/config.js +++ b/lib/config.js @@ -21,11 +21,11 @@ module.exports = { return result.length; } }, - custom_integrations: { + customIntegrations: { currentCountQuery: async (db) => { let result = await db.knex('integrations').count('id', {as: 'count'}).whereNotIn('type', ['internal', 'builtin']).first(); return result.count; } }, - custom_themes: {} + customThemes: {} }; diff --git a/lib/limit-service.js b/lib/limit-service.js index e2f3f355947..6ab6b85e67c 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -10,6 +10,8 @@ class LimitService { loadLimits({limits, helpLink, db}) { Object.keys(limits).forEach((name) => { + name = _.camelCase(name); + if (config[name]) { let limitConfig = _.merge({}, limits[name], config[name]); @@ -23,7 +25,7 @@ class LimitService { } isLimited(limitName) { - return !!this.limits[limitName]; + return !!this.limits[_.camelCase(limitName)]; } async checkIsOverLimit(limitName) { diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 19c1031dd44..8b59e3d8edf 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -2,6 +2,9 @@ // const testUtils = require('./utils'); require('./utils'); +const LimitService = require('../lib/limit-service'); +const {MaxLimit, FlagLimit} = require('../lib/limit'); + describe('Limit Service', function () { describe('Lodash Template', function () { it('Does not get clobbered by this lib', function () { @@ -14,8 +17,6 @@ describe('Limit Service', function () { describe('Error Messages', function () { it('Formats numbers correctly', function () { - const {MaxLimit} = require('../lib/limit'); - let limit = new MaxLimit({name: 'test', config: {max: 35000000, currentCountQuery: () => {}, error: 'Your plan supports up to {{max}} staff users. Please upgrade to add more.'}}); let error = limit.generateError(35000001); @@ -25,4 +26,63 @@ describe('Limit Service', function () { error.errorDetails.total.should.eql(35000001); }); }); + + describe('Loader', function () { + it('can load a basic limit', function () { + const limitService = new LimitService(); + + let limits = {staff: {max: 2}}; + + limitService.loadLimits({limits}); + + limitService.limits.should.be.an.Object().with.properties(['staff']); + limitService.limits.staff.should.be.an.instanceOf(MaxLimit); + limitService.isLimited('staff').should.be.true(); + limitService.isLimited('members').should.be.false(); + }); + + it('can load multiple limits', function () { + const limitService = new LimitService(); + + let limits = {staff: {max: 2}, members: {max: 100}}; + + limitService.loadLimits({limits}); + + limitService.limits.should.be.an.Object().with.properties(['staff', 'members']); + limitService.limits.staff.should.be.an.instanceOf(MaxLimit); + limitService.limits.members.should.be.an.instanceOf(MaxLimit); + limitService.isLimited('staff').should.be.true(); + limitService.isLimited('members').should.be.true(); + }); + + it('can load camel cased limits', function () { + const limitService = new LimitService(); + + let limits = {customThemes: {disabled: true}}; + + limitService.loadLimits({limits}); + + limitService.limits.should.be.an.Object().with.properties(['customThemes']); + limitService.limits.customThemes.should.be.an.instanceOf(FlagLimit); + limitService.isLimited('staff').should.be.false(); + limitService.isLimited('members').should.be.false(); + limitService.isLimited('custom_themes').should.be.true(); + limitService.isLimited('customThemes').should.be.true(); + }); + + it('can load incorrectly cased limits', function () { + const limitService = new LimitService(); + + let limits = {custom_themes: {disabled: true}}; + + limitService.loadLimits({limits}); + + limitService.limits.should.be.an.Object().with.properties(['customThemes']); + limitService.limits.customThemes.should.be.an.instanceOf(FlagLimit); + limitService.isLimited('staff').should.be.false(); + limitService.isLimited('members').should.be.false(); + limitService.isLimited('custom_themes').should.be.true(); + limitService.isLimited('customThemes').should.be.true(); + }); + }); }); From 0c063a73b544f3e8a03fab10d6fe15c39c1b0e1d Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 4 Mar 2021 20:46:49 +0000 Subject: [PATCH 010/255] Published new versions - @tryghost/limit-service@0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90250d7591d..c6c99108565 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.2.1", + "version": "0.3.0", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From e0769c3764b5b49d0f1eadf0dd0d4d14c1078660 Mon Sep 17 00:00:00 2001 From: naz Date: Wed, 31 Mar 2021 13:05:46 +1300 Subject: [PATCH 011/255] Updated readme with module description refs https://github.com/TryGhost/Team/issues/510 - Explained the intention and responsibility ares of the module --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2a2acae7c2..bfc72cfb711 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ # Limit Service +This module is intended to hold **all of the logic** for testing if site: +- would be over a given limit if they took an action (i.e. added one more thing, switched to a different limit) +- if they are over a limit already +- consistent error messages explaining why the limit has been reached ## Install @@ -36,4 +40,4 @@ Follow the instructions for the top-level repo. # Copyright & License -Copyright (c) 2013-2021 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2021 Ghost Foundation - Released under the [MIT license](LICENSE). From 0656c278b0f7985996f2fa8764662bab5971a16d Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 17:17:45 +1300 Subject: [PATCH 012/255] Added test coverage for max limit class refs https://github.com/TryGhost/Team/issues/587 - Test were missing for class initialization and around how the limit currently works. - Before extending it's behavior throught its valuable to cover current functionality to not accidentally break anything --- test/limit.test.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 test/limit.test.js diff --git a/test/limit.test.js b/test/limit.test.js new file mode 100644 index 00000000000..dbb6d0e2cb4 --- /dev/null +++ b/test/limit.test.js @@ -0,0 +1,57 @@ +// Switch these lines once there are useful utils +// const testUtils = require('./utils'); +require('./utils'); + +const {MaxLimit} = require('../lib/limit'); + +describe('Limit Service', function () { + describe('Max Limit', function () { + it('throws if initialized without a current count query', function () { + const config = {}; + + try { + const limit = new MaxLimit({name: 'no accountability!', config}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + } + }); + + it('throws if would go over the limit', async function () { + const config = { + max: 1, + currentCountQuery: () => 1 + }; + const limit = new MaxLimit({name: 'maxy', config}); + + try { + await limit.errorIfWouldGoOverLimit(); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); + + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'maxy'); + + should.exist(err.message); + should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); + } + }); + + it('passes if does not go over the limit', async function () { + const config = { + max: 2, + currentCountQuery: () => 1 + }; + + const limit = new MaxLimit({name: 'maxy', config}); + + await limit.errorIfWouldGoOverLimit(); + }); + }); +}); From 999cc1afa5df042433c3822b77784fb5a039f126 Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 17:20:56 +1300 Subject: [PATCH 013/255] Added incorrect 'max' usage error to MaxLimit refs https://github.com/TryGhost/Team/issues/587 - When the 'max' configuration is missing the instance of the class breaks when used unexpectedly. Followed similar approach to currentCountQuery check by failing fast in the constructor --- lib/limit.js | 4 ++++ test/limit.test.js | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/limit.js b/lib/limit.js index 5be33cf4fba..a767ed7a424 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -31,6 +31,10 @@ class MaxLimit extends Limit { constructor({name, config, helpLink, db}) { super({name, error: config.error || '', helpLink, db}); + if (config.max === undefined) { + throw new errors.IncorrectUsageError('Attempted to setup a max limit without a limit'); + } + if (!config.currentCountQuery) { throw new errors.IncorrectUsageError('Attempted to setup a max limit without a current count query'); } diff --git a/test/limit.test.js b/test/limit.test.js index dbb6d0e2cb4..3e771cf363e 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -6,6 +6,19 @@ const {MaxLimit} = require('../lib/limit'); describe('Limit Service', function () { describe('Max Limit', function () { + it('throws if initialized without a max limit', function () { + const config = {}; + + try { + const limit = new MaxLimit({name: 'no limits!', config}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + } + }); + it('throws if initialized without a current count query', function () { const config = {}; From eda26ecebe77203e537f289fae45d81883ee665a Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 17:29:26 +1300 Subject: [PATCH 014/255] Added JSDoc to MaxLimit constructor refs https://github.com/TryGhost/Team/issues/587 - Having a JSDoc gives better intellisense when the class is instantiated and provides clues about what each parameter might be used for --- lib/limit.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/limit.js b/lib/limit.js index a767ed7a424..ca2f7a1cf80 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -28,6 +28,16 @@ class Limit { } 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 {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 + */ constructor({name, config, helpLink, db}) { super({name, error: config.error || '', helpLink, db}); From 69feda0d4f329d9aa726fc20c8b7446859eac7ef Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 17:59:52 +1300 Subject: [PATCH 015/255] Added optional max limit override to errorIfWouldGoOverLimit refs https://github.com/TryGhost/Team/issues/587 - The optional {max} passed as an option allows to override currently configured limit and do a theoretical new limit check. For example: check if the max limit will be exceeded if the limit changes (user changes plans) --- lib/limit.js | 12 ++++++++++-- test/limit.test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index ca2f7a1cf80..4731d61f17c 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -81,12 +81,20 @@ class MaxLimit extends Limit { return await this.currentCountQueryFn(this.db); } - async errorIfWouldGoOverLimit() { + /** + * 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 errorIfWouldGoOverLimit({max} = {}) { let currentCount = await this.currentCountQuery(this.db); - if ((currentCount + 1) > this.max) { + + if ((currentCount + 1) > (max || this.max)) { throw this.generateError(currentCount); } } + async errorIfIsOverLimit() { let currentCount = await this.currentCountQuery(this.db); if (currentCount > this.max) { diff --git a/test/limit.test.js b/test/limit.test.js index 3e771cf363e..fc57babdddf 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -66,5 +66,34 @@ describe('Limit Service', function () { await limit.errorIfWouldGoOverLimit(); }); + + it('ignores default configured max limit when it is passed explicitly', async function () { + const config = { + max: 10, + currentCountQuery: () => 10 + }; + + const limit = new MaxLimit({name: 'maxy', config}); + + // should pass as the limit is overridden to 10 + 1 = 11 + await limit.errorIfWouldGoOverLimit({max: 11}); + + try { + // should fail because limit is overridden to 10 + 1 < 1 + await limit.errorIfWouldGoOverLimit({max: 1}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); + + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'maxy'); + + should.exist(err.message); + should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); + } + }); }); }); From a9be63fefbd4ba0be9cfecbff9f030274ecab386 Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 18:03:32 +1300 Subject: [PATCH 016/255] Grouped MaxLimit test cases into describe groups refs https://github.com/TryGhost/Team/issues/587 - Clenup before adding even more test coverage --- test/limit.test.js | 148 +++++++++++++++++++++++---------------------- 1 file changed, 76 insertions(+), 72 deletions(-) diff --git a/test/limit.test.js b/test/limit.test.js index fc57babdddf..e15a611f5c3 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -6,94 +6,98 @@ const {MaxLimit} = require('../lib/limit'); describe('Limit Service', function () { describe('Max Limit', function () { - it('throws if initialized without a max limit', function () { - const config = {}; - - try { - const limit = new MaxLimit({name: 'no limits!', config}); - should.fail(limit, 'Should have errored'); - } catch (err) { - should.exist(err); - should.exist(err.errorType); - should.equal(err.errorType, 'IncorrectUsageError'); - } + describe('Constructor', function () { + it('throws if initialized without a max limit', function () { + const config = {}; + + try { + const limit = new MaxLimit({name: 'no limits!', config}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + } + }); + + it('throws if initialized without a current count query', function () { + const config = {}; + + try { + const limit = new MaxLimit({name: 'no accountability!', config}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + } + }); }); - it('throws if initialized without a current count query', function () { - const config = {}; - - try { - const limit = new MaxLimit({name: 'no accountability!', config}); - should.fail(limit, 'Should have errored'); - } catch (err) { - should.exist(err); - should.exist(err.errorType); - should.equal(err.errorType, 'IncorrectUsageError'); - } - }); - - it('throws if would go over the limit', async function () { - const config = { - max: 1, - currentCountQuery: () => 1 - }; - const limit = new MaxLimit({name: 'maxy', config}); + describe('Would go over limit', function () { + it('throws if would go over the limit', async function () { + const config = { + max: 1, + currentCountQuery: () => 1 + }; + const limit = new MaxLimit({name: 'maxy', config}); - try { - await limit.errorIfWouldGoOverLimit(); - should.fail(limit, 'Should have errored'); - } catch (err) { - should.exist(err); + try { + await limit.errorIfWouldGoOverLimit(); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); - should.exist(err.errorType); - should.equal(err.errorType, 'HostLimitError'); + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); - should.exist(err.errorDetails); - should.equal(err.errorDetails.name, 'maxy'); + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'maxy'); - should.exist(err.message); - should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); - } - }); + should.exist(err.message); + should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); + } + }); - it('passes if does not go over the limit', async function () { - const config = { - max: 2, - currentCountQuery: () => 1 - }; + it('passes if does not go over the limit', async function () { + const config = { + max: 2, + currentCountQuery: () => 1 + }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config}); - await limit.errorIfWouldGoOverLimit(); - }); + await limit.errorIfWouldGoOverLimit(); + }); - it('ignores default configured max limit when it is passed explicitly', async function () { - const config = { - max: 10, - currentCountQuery: () => 10 - }; + it('ignores default configured max limit when it is passed explicitly', async function () { + const config = { + max: 10, + currentCountQuery: () => 10 + }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config}); - // should pass as the limit is overridden to 10 + 1 = 11 - await limit.errorIfWouldGoOverLimit({max: 11}); + // should pass as the limit is overridden to 10 + 1 = 11 + await limit.errorIfWouldGoOverLimit({max: 11}); - try { - // should fail because limit is overridden to 10 + 1 < 1 - await limit.errorIfWouldGoOverLimit({max: 1}); - should.fail(limit, 'Should have errored'); - } catch (err) { - should.exist(err); + try { + // should fail because limit is overridden to 10 + 1 < 1 + await limit.errorIfWouldGoOverLimit({max: 1}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); - should.exist(err.errorType); - should.equal(err.errorType, 'HostLimitError'); + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); - should.exist(err.errorDetails); - should.equal(err.errorDetails.name, 'maxy'); + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'maxy'); - should.exist(err.message); - should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); - } + should.exist(err.message); + should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); + } + }); }); }); }); From 61e190ea8e0325ec9aa3d2c1bc1280d77048205f Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 18:10:07 +1300 Subject: [PATCH 017/255] Added test coverage for is over limit check refs https://github.com/TryGhost/Team/issues/587 - There was no test coverage for MaxLimit's errorIfIsOverLimit check. Added basic test to make sure upcoming modifications don't break existing functionality --- test/limit.test.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/limit.test.js b/test/limit.test.js index e15a611f5c3..4d2f8c82a5b 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -34,6 +34,43 @@ describe('Limit Service', function () { }); }); + describe('Is over limit', function () { + it('throws if is over the limit', async function () { + const config = { + max: 3, + currentCountQuery: () => 42 + }; + const limit = new MaxLimit({name: 'maxy', config}); + + try { + await limit.errorIfIsOverLimit(); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); + + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'maxy'); + + should.exist(err.message); + should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); + } + }); + + it('passes if does not go over the limit', async function () { + const config = { + max: 1, + currentCountQuery: () => 1 + }; + + const limit = new MaxLimit({name: 'maxy', config}); + + await limit.errorIfIsOverLimit(); + }); + }); + describe('Would go over limit', function () { it('throws if would go over the limit', async function () { const config = { From 033611812ec15968241acff760375fa9556ef1ee Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 18:27:29 +1300 Subject: [PATCH 018/255] Added optional max limit override to errorIfIsOverLimit refs https://github.com/TryGhost/Team/issues/587 refs https://github.com/TryGhost/Utils/commit/d086823f8034513f026d46886c755d6f18c1179e - It's a symmetric change to the one introduce in the refenreced commit - TLDR: allows to check if limit was reached if the user changes the limit --- lib/limit.js | 11 +++++++++-- test/limit.test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index 4731d61f17c..44287cafc7c 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -95,9 +95,16 @@ class MaxLimit extends Limit { } } - async errorIfIsOverLimit() { + /** + * 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} = {}) { let currentCount = await this.currentCountQuery(this.db); - if (currentCount > this.max) { + + if (currentCount > (max || this.max)) { throw this.generateError(currentCount); } } diff --git a/test/limit.test.js b/test/limit.test.js index 4d2f8c82a5b..ed6493d82bd 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -69,6 +69,35 @@ describe('Limit Service', function () { await limit.errorIfIsOverLimit(); }); + + it('ignores default configured max limit when it is passed explicitly', async function () { + const config = { + max: 10, + currentCountQuery: () => 10 + }; + + const limit = new MaxLimit({name: 'maxy', config}); + + // should pass as the limit is exactly on the limit 10 >= 10 + await limit.errorIfIsOverLimit({max: 10}); + + try { + // should fail because limit is overridden to 10 < 9 + await limit.errorIfIsOverLimit({max: 9}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); + + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'maxy'); + + should.exist(err.message); + should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); + } + }); }); describe('Would go over limit', function () { From 8d4ae05528218e0e5df6f1d4a903e0d47b921639 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 1 Apr 2021 09:06:05 +0000 Subject: [PATCH 019/255] Update Test & linting packages --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6c99108565..2b7baa16baf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "mocha": "8.3.0", + "mocha": "8.3.2", "should": "13.2.3", "sinon": "9.2.4" }, From 3d60ba416ae5c5439267934d998368447700c160 Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 1 Apr 2021 23:06:09 +1300 Subject: [PATCH 020/255] Added docs for limit service common ussecases refs https://github.com/TryGhost/Team/issues/587 - Documented common usecases such as: 1. initialization and configuration of limit service 2. usage of "max" types of limits --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bfc72cfb711..e1a6959b337 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,84 @@ or ## Usage - +Below is a sample code to wire up limit service and perform few common limit checks: + +```js +const LimitService = require('@tryghost/limit-service'); + +// create a LimitService instance +const limitService = new LimitService(); + +// setup limit configuration +// currently supported limit keys are: staff, members, customThemes, customIntegrations +// all limit configs support custom "error" configuration that is a template string +const limits = { + // staff and member are "max" type of limits accepting "max" configuration + staff: { + max: 1, + error: 'Your plan supports up to {{max}} staff users. Please upgrade to add more.' + }, + members: { + max: 1000, + error: 'Your plan supports up to {{max}} members. Please upgrade to reenable publishing.' + }, + // customThemes and customIntegrations are "flag" type of limits accepting disabled boolean configuration + customThemes: { + disabled: true, + error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.' + }, + customIntegrations: { + disabled: true, + error: 'You can use all our official, built-in integrations on the Starter plan. If you upgrade to one of our higher tiers, you’ll also be able to create and edit custom integrations and API keys for advanced workflows.' + } +}; + +// initialize the URL linking to help documentation etc. +const helpLink = 'https://ghost.org/help/'; + +// initialize knex db connection for the limit service to use when running query checks +const db = knex({ + client: 'mysql', + connection: { + user: 'root', + password: 'toor', + host: 'localhost', + database: 'ghost', + } +}); + +// finish initializing the limits service +limitService.loadLimits({limits, db, helpLink}); + +// perform limit checks + +// check if there is a 'staff' limit configured +if (limitService.isLimited('staff')) { + // throws an error if current 'staff' limit **would** go over the limit set up in configuration (max:1) + await limitService.errorIfWouldGoOverLimit('staff'); + + // same as above but overrides the default max check from max of 1 to 100 + // useful in cases you need to check if specific instance would still be over the limit if the limit changed + await limitService.errorIfWouldGoOverLimit('staff', {max: 100}); +} + +// "max" types of limits have currentCountQuery method reguring a number that is currently in use for the limit +// for example it could be 1, 3, 5 or whatever amount of 'staff' is currently in the system +const staffCount = await limitService.currentCountQuery('staff'); + +// do something with that number +console.log(`Your current staff count is at: ${staffCount}!`); + +// check if there is a 'members' limit configured +if (limitService.isLimited('members')) { + // throws an error if current 'staff' limit **is** over the limit set up in configuration (max: 1000) + await limitService.errorIfIsOverLimit('members'); + + // same as above but overrides the default max check from max of 1000 to 10000 + // useful in cases you need to check if specific instance would still be over the limit if the limit changed + await limitService.errorIfIsOverLimit('members', {max: 10000}); +} +``` ## Develop From 556aa582798f2f160b7c0d474fcbae5f6d1a8b07 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 1 Apr 2021 23:49:59 +0000 Subject: [PATCH 021/255] Update dependency sinon to v10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b7baa16baf..65b04dfedc2 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "mocha": "8.3.2", "should": "13.2.3", - "sinon": "9.2.4" + "sinon": "10.0.0" }, "dependencies": { "lodash": "^4.17.21" From c6c3933f319a65c7e80f0be74cc58525b9c5e882 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 5 Apr 2021 16:02:35 +1200 Subject: [PATCH 022/255] Added JSDoc to loadLimits method refs https://github.com/TryGhost/Team/issues/597 - Before adding more parameters documented existing ones - Created LimitConfig type definition to have easier look into the structure of limit conifiguration --- lib/limit-service.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/limit-service.js b/lib/limit-service.js index 6ab6b85e67c..7b9d2d4842f 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -8,11 +8,20 @@ class LimitService { this.limits = {}; } + /** + * Initializes the limits based on configuration + * + * @param {Object} options + * @param {Object} options.limits - hash containing limit configurations keyed by limit name and containing + * @param {String} options.helpLink - URL pointing to help resources for when limit is reached + * @param {Object} options.db - knex db connection instance or other data source for the limit checks + */ loadLimits({limits, helpLink, db}) { Object.keys(limits).forEach((name) => { name = _.camelCase(name); if (config[name]) { + /** @type LimitConfig */ let limitConfig = _.merge({}, limits[name], config[name]); if (_.has(limitConfig, 'max')) { @@ -76,3 +85,10 @@ class LimitService { } module.exports = LimitService; + +/** + * @typedef {Object} LimitConfig + * @prop {Number} [max] - max limit + * @prop {Boolean} [disabled] - flag disabling/enabling limit + * @prop {String} error - custom error to be displayed when the limit is reached + */ From e5a378f822ac22f48c67ab2e47284cbfabf57262 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 5 Apr 2021 16:03:36 +1200 Subject: [PATCH 023/255] Added JSDoc to FlagLimit constructor refs https://github.com/TryGhost/Team/issues/597 - Before adding more parameters documented existing ones --- lib/limit.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/limit.js b/lib/limit.js index 44287cafc7c..aa555e2bc38 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -111,6 +111,15 @@ class MaxLimit extends Limit { } 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.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 + */ constructor({name, config, helpLink, db}) { super({name, error: config.error || '', helpLink, db}); From da0816945c42eee1564cc426ce3fb5ad5a007fc8 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 5 Apr 2021 16:17:57 +1200 Subject: [PATCH 024/255] Removed ghost-ignition's errors dependency refs https://github.com/TryGhost/Team/issues/597 - To be able to transpile the library for different runtimes (make it polymorphic) had to get rid of dependencies that were not compatible with ES Modules - By making errors an injectable constructor option it removes the depencency and allows to transpile the library for multiple targets - The `errors` option is now a required parameter for `loadLimits` method. It errors if it's missing (error message copy inspired by content api error https://github.com/TryGhost/SDK/blob/69fcea0582ebfd871a62588877db9da74a1194fe/packages/content-api/lib/index.js#L21) --- lib/limit-service.js | 18 ++++++++++++------ lib/limit.js | 19 ++++++++++--------- test/fixtures/errors.js | 25 +++++++++++++++++++++++++ test/limit-service.test.js | 34 +++++++++++++++++++++++++++++----- test/limit.test.js | 17 +++++++++-------- 5 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 test/fixtures/errors.js diff --git a/lib/limit-service.js b/lib/limit-service.js index 7b9d2d4842f..f56b274f746 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -1,4 +1,3 @@ -const errors = require('@tryghost/errors'); const {MaxLimit, FlagLimit} = require('./limit'); const config = require('./config'); const _ = require('lodash'); @@ -15,8 +14,15 @@ class LimitService { * @param {Object} options.limits - hash containing limit configurations keyed by limit name and containing * @param {String} options.helpLink - URL pointing to help resources for when limit is reached * @param {Object} options.db - knex db connection instance or other data source for the limit checks + * @param {Object} options.errors - instance of errors compatible with Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) */ - loadLimits({limits, helpLink, db}) { + loadLimits({limits, helpLink, db, errors}) { + if (!errors) { + throw new Error(`Config Missing: 'errors' is required. `); + } + + this.errors = errors; + Object.keys(limits).forEach((name) => { name = _.camelCase(name); @@ -25,9 +31,9 @@ class LimitService { let limitConfig = _.merge({}, limits[name], config[name]); if (_.has(limitConfig, 'max')) { - this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db}); + this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db, errors}); } else { - this.limits[name] = new FlagLimit({name: name, config: limitConfig, helpLink}); + this.limits[name] = new FlagLimit({name: name, config: limitConfig, helpLink, errors}); } } }); @@ -46,7 +52,7 @@ class LimitService { await this.limits[limitName].errorIfIsOverLimit(); return false; } catch (error) { - if (error instanceof errors.HostLimitError) { + if (error instanceof this.errors.HostLimitError) { return true; } } @@ -61,7 +67,7 @@ class LimitService { await this.limits[limitName].errorIfWouldGoOverLimit(); return false; } catch (error) { - if (error instanceof errors.HostLimitError) { + if (error instanceof this.errors.HostLimitError) { return true; } } diff --git a/lib/limit.js b/lib/limit.js index aa555e2bc38..034a373bd14 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -1,15 +1,14 @@ -const errors = require('@tryghost/errors'); - // run in context allows us to change the templateSettings without causing havoc const _ = require('lodash').runInContext(); _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; class Limit { - constructor({name, error, helpLink, db}) { + constructor({name, error, helpLink, db, errors}) { this.name = name; this.error = error; this.helpLink = helpLink; this.db = db; + this.errors = errors; } generateError() { @@ -37,9 +36,10 @@ class MaxLimit extends Limit { * @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 + * @param {Object} options.errors - instance of errors compatible with Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) */ - constructor({name, config, helpLink, db}) { - super({name, error: config.error || '', helpLink, db}); + constructor({name, config, helpLink, db, errors}) { + super({name, error: config.error || '', helpLink, db, errors}); if (config.max === undefined) { throw new errors.IncorrectUsageError('Attempted to setup a max limit without a limit'); @@ -74,7 +74,7 @@ class MaxLimit extends Limit { errorObj.errorDetails.limit = this.max; errorObj.errorDetails.total = count; - return new errors.HostLimitError(errorObj); + return new this.errors.HostLimitError(errorObj); } async currentCountQuery() { @@ -119,9 +119,10 @@ class FlagLimit extends Limit { * @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 + * @param {Object} options.errors - instance of errors compatible with Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) */ - constructor({name, config, helpLink, db}) { - super({name, error: config.error || '', helpLink, db}); + 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)}.`; @@ -136,7 +137,7 @@ class FlagLimit extends Limit { errorObj.message = this.fallbackMessage; } - return new errors.HostLimitError(errorObj); + return new this.errors.HostLimitError(errorObj); } /** diff --git a/test/fixtures/errors.js b/test/fixtures/errors.js new file mode 100644 index 00000000000..5968fa03539 --- /dev/null +++ b/test/fixtures/errors.js @@ -0,0 +1,25 @@ +class Error { + constructor({errorType, errorDetails, message}) { + this.errorType = errorType; + this.errorDetails = errorDetails; + this.message = message; + } +} + +class IncorrectUsageError extends Error { + constructor(options) { + super(Object.assign({errorType: 'IncorrectUsageError'}, options)); + } +} + +class HostLimitError extends Error { + constructor(options) { + super(Object.assign({errorType: 'HostLimitError'}, options)); + } +} + +// NOTE: this module is here to serve as a dummy fixture for Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) +module.exports = { + IncorrectUsageError, + HostLimitError +}; diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 8b59e3d8edf..8db30c3fffd 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -5,6 +5,8 @@ require('./utils'); const LimitService = require('../lib/limit-service'); const {MaxLimit, FlagLimit} = require('../lib/limit'); +const errors = require('./fixtures/errors'); + describe('Limit Service', function () { describe('Lodash Template', function () { it('Does not get clobbered by this lib', function () { @@ -17,7 +19,15 @@ describe('Limit Service', function () { describe('Error Messages', function () { it('Formats numbers correctly', function () { - let limit = new MaxLimit({name: 'test', config: {max: 35000000, currentCountQuery: () => {}, error: 'Your plan supports up to {{max}} staff users. Please upgrade to add more.'}}); + let limit = new MaxLimit({ + name: 'test', + config: { + max: 35000000, + currentCountQuery: () => {}, + error: 'Your plan supports up to {{max}} staff users. Please upgrade to add more.' + }, + errors + }); let error = limit.generateError(35000001); @@ -28,12 +38,26 @@ describe('Limit Service', function () { }); describe('Loader', function () { + it('throws if errors configuration is not specified', function () { + const limitService = new LimitService(); + + let limits = {staff: {max: 2}}; + + try { + limitService.loadLimits({limits}); + should.fail(limitService, 'Should have errored'); + } catch (err) { + should.exist(err); + err.message.should.equal(`Config Missing: 'errors' is required`); + } + }); + it('can load a basic limit', function () { const limitService = new LimitService(); let limits = {staff: {max: 2}}; - limitService.loadLimits({limits}); + limitService.loadLimits({limits, errors}); limitService.limits.should.be.an.Object().with.properties(['staff']); limitService.limits.staff.should.be.an.instanceOf(MaxLimit); @@ -46,7 +70,7 @@ describe('Limit Service', function () { let limits = {staff: {max: 2}, members: {max: 100}}; - limitService.loadLimits({limits}); + limitService.loadLimits({limits, errors}); limitService.limits.should.be.an.Object().with.properties(['staff', 'members']); limitService.limits.staff.should.be.an.instanceOf(MaxLimit); @@ -60,7 +84,7 @@ describe('Limit Service', function () { let limits = {customThemes: {disabled: true}}; - limitService.loadLimits({limits}); + limitService.loadLimits({limits, errors}); limitService.limits.should.be.an.Object().with.properties(['customThemes']); limitService.limits.customThemes.should.be.an.instanceOf(FlagLimit); @@ -75,7 +99,7 @@ describe('Limit Service', function () { let limits = {custom_themes: {disabled: true}}; - limitService.loadLimits({limits}); + limitService.loadLimits({limits, errors}); limitService.limits.should.be.an.Object().with.properties(['customThemes']); limitService.limits.customThemes.should.be.an.instanceOf(FlagLimit); diff --git a/test/limit.test.js b/test/limit.test.js index ed6493d82bd..9e5629d15fa 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -2,6 +2,7 @@ // const testUtils = require('./utils'); require('./utils'); +const errors = require('./fixtures/errors'); const {MaxLimit} = require('../lib/limit'); describe('Limit Service', function () { @@ -11,7 +12,7 @@ describe('Limit Service', function () { const config = {}; try { - const limit = new MaxLimit({name: 'no limits!', config}); + const limit = new MaxLimit({name: 'no limits!', config, errors}); should.fail(limit, 'Should have errored'); } catch (err) { should.exist(err); @@ -24,7 +25,7 @@ describe('Limit Service', function () { const config = {}; try { - const limit = new MaxLimit({name: 'no accountability!', config}); + const limit = new MaxLimit({name: 'no accountability!', config, errors}); should.fail(limit, 'Should have errored'); } catch (err) { should.exist(err); @@ -40,7 +41,7 @@ describe('Limit Service', function () { max: 3, currentCountQuery: () => 42 }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config, errors}); try { await limit.errorIfIsOverLimit(); @@ -65,7 +66,7 @@ describe('Limit Service', function () { currentCountQuery: () => 1 }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config, errors}); await limit.errorIfIsOverLimit(); }); @@ -76,7 +77,7 @@ describe('Limit Service', function () { currentCountQuery: () => 10 }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config, errors}); // should pass as the limit is exactly on the limit 10 >= 10 await limit.errorIfIsOverLimit({max: 10}); @@ -106,7 +107,7 @@ describe('Limit Service', function () { max: 1, currentCountQuery: () => 1 }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config, errors}); try { await limit.errorIfWouldGoOverLimit(); @@ -131,7 +132,7 @@ describe('Limit Service', function () { currentCountQuery: () => 1 }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config, errors}); await limit.errorIfWouldGoOverLimit(); }); @@ -142,7 +143,7 @@ describe('Limit Service', function () { currentCountQuery: () => 10 }; - const limit = new MaxLimit({name: 'maxy', config}); + const limit = new MaxLimit({name: 'maxy', config, errors}); // should pass as the limit is overridden to 10 + 1 = 11 await limit.errorIfWouldGoOverLimit({max: 11}); From c4443f158647cb00de42871f699d2eb9df17e112 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 5 Apr 2021 16:21:35 +1200 Subject: [PATCH 025/255] Updated docs/examples with errors parameter refs https://github.com/TryGhost/Team/issues/597 refs https://github.com/TryGhost/Utils/commit/170e6a0a46c09c042372cb993aba43ffc240c4be - As errors dependency has been removed in refed commit, updated the docs with correct usage of the library. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e1a6959b337..174652efacb 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ or Below is a sample code to wire up limit service and perform few common limit checks: ```js +const errors = require('@tryghost/errors'); const LimitService = require('@tryghost/limit-service'); // create a LimitService instance @@ -61,7 +62,7 @@ const db = knex({ }); // finish initializing the limits service -limitService.loadLimits({limits, db, helpLink}); +limitService.loadLimits({limits, db, helpLink, errors}); // perform limit checks From 5e0cd6c1bb2ea6b56bfc89baba4cf1dda95d77a3 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 5 Apr 2021 16:29:07 +1200 Subject: [PATCH 026/255] Fixed test missing a whitespace refs https://github.com/TryGhost/Team/issues/597 refs https://github.com/TryGhost/Utils/commit/170e6a0a46c09c042372cb993aba43ffc240c4be --- lib/limit-service.js | 2 +- test/limit-service.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index f56b274f746..ecc94aa3c67 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -18,7 +18,7 @@ class LimitService { */ loadLimits({limits, helpLink, db, errors}) { if (!errors) { - throw new Error(`Config Missing: 'errors' is required. `); + throw new Error(`Config Missing: 'errors' is required.`); } this.errors = errors; diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 8db30c3fffd..3df84659bb8 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -48,7 +48,7 @@ describe('Limit Service', function () { should.fail(limitService, 'Should have errored'); } catch (err) { should.exist(err); - err.message.should.equal(`Config Missing: 'errors' is required`); + err.message.should.eql(`Config Missing: 'errors' is required.`); } }); From 37964f353eb4f778b929a0d21800ecd3dcafe3fe Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 6 Apr 2021 16:45:46 +1200 Subject: [PATCH 027/255] =?UTF-8?q?=E2=9C=A8=20Added=20custom=20count=20qu?= =?UTF-8?q?eries=20for=20"max"=20limits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs https://github.com/TryGhost/Team/issues/597 - When the library is used on a client without a DB connection (e.g. frontend client running in a browser) the library needs to expose a way to override count queries. - The way these can be used is giving a count based on a HTTP request or some other data provider - Example use with max limit like "staff" would be loading the limit servcie if following way: ``` const limitService = new LimitService(); let limits = { staff: { max: 2, currentCountQuery: () => 5 } }; limitService.loadLimits({limits, errors}); await limitService.checkIsOverLimit('staff') ``` --- lib/limit-service.js | 3 ++- test/limit-service.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index ecc94aa3c67..76bd3f5c91a 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -28,7 +28,7 @@ class LimitService { if (config[name]) { /** @type LimitConfig */ - let limitConfig = _.merge({}, limits[name], config[name]); + let limitConfig = Object.assign({}, config[name], limits[name]); if (_.has(limitConfig, 'max')) { this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db, errors}); @@ -97,4 +97,5 @@ module.exports = LimitService; * @prop {Number} [max] - max limit * @prop {Boolean} [disabled] - flag disabling/enabling limit * @prop {String} error - custom error to be displayed when the limit is reached + * @prop {Function} [currentCountQuery] - function returning count for the "max" type of limit */ diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 3df84659bb8..f709f767145 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -109,4 +109,29 @@ describe('Limit Service', function () { limitService.isLimited('customThemes').should.be.true(); }); }); + + describe('Custom limit count query configuration', function () { + it('can use a custom implementation of max limit query', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 5 + }, + members: { + max: 100, + currentCountQuery: () => 100 + } + }; + + limitService.loadLimits({limits, errors}); + + (await limitService.checkIsOverLimit('staff')).should.be.true(); + (await limitService.checkWouldGoOverLimit('staff')).should.be.true(); + + (await limitService.checkIsOverLimit('members')).should.be.false(); + (await limitService.checkWouldGoOverLimit('members')).should.be.true(); + }); + }); }); From a18c86d19e8fda790f4122f15933869ab6367c23 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 6 Apr 2021 17:05:44 +1200 Subject: [PATCH 028/255] Added docs for currentCountQuery usage refs https://github.com/TryGhost/Team/issues/597 - Documented example usacase for currentCountQuery override intoruced in previous commit --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 174652efacb..0f3ba90f8f7 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,24 @@ if (limitService.isLimited('members')) { } ``` +In case the limit check is run without direct access to the database you can override `currentCountQuery` functions for each "max" type of limit. An example usecase would be a frontend client running in a browser. A browser client can check the limit data through HTTP request and then provide that data to the limit service. Example code to do exactly that: +``` +const limitService = new LimitService(); + +let limits = { + staff: { + max: 2, + currentCountQuery: async () => (await fetch('/api/staff')).json().length + } +}; + +limitService.loadLimits({limits, errors}); + +if (await limitService.checkIsOverLimit('staff')) { + // do something as "staff" limit has been reached +}; +``` + ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). From e83b3c246d508c7f463209aec7e7d0c07763713f Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 7 Apr 2021 13:31:42 +1200 Subject: [PATCH 029/255] Added test coverage for {{max}} and {{count}} refs https://github.com/TryGhost/Team/issues/510 - {{max}} and {{count}} variable usage was not covered but had valid usecases in the library client's, so considered to "document" them through tests - For more context these variables are available in custom `error` templates that are provided with each limit --- test/limit-service.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/limit-service.test.js b/test/limit-service.test.js index f709f767145..e79449c8075 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -35,6 +35,24 @@ describe('Limit Service', function () { error.errorDetails.limit.should.eql(35000000); error.errorDetails.total.should.eql(35000001); }); + + it('Supports {{max}} and {{count}} variables', function () { + let limit = new MaxLimit({ + name: 'test', + config: { + max: 5, + currentCountQuery: () => {}, + error: 'Your plan supports up to {{max}} staff users. You are currently at {{count}} staff users.Please upgrade to add more.' + }, + errors + }); + + let error = limit.generateError(7); + + error.message.should.eql('Your plan supports up to 5 staff users. You are currently at 7 staff users.Please upgrade to add more.'); + error.errorDetails.limit.should.eql(5); + error.errorDetails.total.should.eql(7); + }); }); describe('Loader', function () { From 5fb3249fee8d0e7230f5700818ff62b9a7d70427 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 7 Apr 2021 13:47:32 +1200 Subject: [PATCH 030/255] Published new versions - @tryghost/adapter-manager@0.2.10 - @tryghost/bootstrap-socket@0.2.8 - @tryghost/constants@0.1.7 - @tryghost/errors@0.2.10 - @tryghost/image-transform@1.0.10 - @tryghost/job-manager@0.8.2 - @tryghost/limit-service@0.4.0 - @tryghost/moleculer-service-from-class@0.2.13 - @tryghost/mw-session-from-token@0.1.17 - @tryghost/pretty-cli@1.2.16 - @tryghost/promise@0.1.7 - @tryghost/release-utils@0.6.13 - @tryghost/security@0.2.7 - @tryghost/session-service@0.1.18 - @tryghost/vhost-middleware@1.0.14 - @tryghost/zip@1.1.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65b04dfedc2..b825983639a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.3.0", + "version": "0.4.0", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From a7e18e9062abfc3147c282cefafff502f7a0da24 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 7 Apr 2021 18:13:10 +1200 Subject: [PATCH 031/255] Improved query formatting refs https://github.com/TryGhost/Team/issues/599 - Oneliners with lots of chained commands are hardly readable on small screens --- lib/config.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index 4c37d967f33..51171344afe 100644 --- a/lib/config.js +++ b/lib/config.js @@ -23,7 +23,11 @@ module.exports = { }, customIntegrations: { currentCountQuery: async (db) => { - let result = await db.knex('integrations').count('id', {as: 'count'}).whereNotIn('type', ['internal', 'builtin']).first(); + let result = await db.knex('integrations') + .count('id', {as: 'count'}) + .whereNotIn('type', ['internal', 'builtin']) + .first(); + return result.count; } }, From 2ca465d6bf6afca20f8721406d29df2149fe3d81 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 7 Apr 2021 18:14:18 +1200 Subject: [PATCH 032/255] Improved docs around {{max}} & {{count}} refs https://github.com/TryGhost/Team/issues/587 - Improved description and provided example use of error message template variables that are available for "MaxLimit" types of limits --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f3ba90f8f7..6e4c5fad2ea 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ if (limitService.isLimited('members')) { ``` In case the limit check is run without direct access to the database you can override `currentCountQuery` functions for each "max" type of limit. An example usecase would be a frontend client running in a browser. A browser client can check the limit data through HTTP request and then provide that data to the limit service. Example code to do exactly that: -``` +```js const limitService = new LimitService(); let limits = { @@ -112,6 +112,17 @@ if (await limitService.checkIsOverLimit('staff')) { }; ``` +### Custom error messages +Errors returned by the limit service can be customized. When configuring the limit service through `loadLimits` method `limits` objects can specify an `error` property that is a template string. Additionally, "MaxLimit" limit type supports following variables- {{count}} and {{max}}. + +An example configuration for "MaxLimit" limit using an error template can look like following: +```json +"staff": { + "max": 5, + "error": "Your plan supports up to {{max}} staff users and you currently have {{count}}. Please upgrade to add more." +} +``` + ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). From 63b5e6a9752e5a19b361d0f9584a4394205146ab Mon Sep 17 00:00:00 2001 From: Thibaut Patel Date: Thu, 8 Apr 2021 15:07:30 +0200 Subject: [PATCH 033/255] Added a test to confirm `isLimited` behavior of an unkown key no issue --- test/limit-service.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/limit-service.test.js b/test/limit-service.test.js index e79449c8075..282c625901d 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -126,6 +126,19 @@ describe('Limit Service', function () { limitService.isLimited('custom_themes').should.be.true(); limitService.isLimited('customThemes').should.be.true(); }); + + it('answers correctly when no limits are provided', function () { + const limitService = new LimitService(); + + let limits = {}; + + limitService.loadLimits({limits, errors}); + + limitService.isLimited('staff').should.be.false(); + limitService.isLimited('members').should.be.false(); + limitService.isLimited('custom_themes').should.be.false(); + limitService.isLimited('customThemes').should.be.false(); + }); }); describe('Custom limit count query configuration', function () { From 905fee4f73de9e3b430b709c7bc2b1b2956d9f3a Mon Sep 17 00:00:00 2001 From: Thibaut Patel Date: Thu, 8 Apr 2021 17:29:53 +0200 Subject: [PATCH 034/255] Added allowlist limit (#144) issue https://github.com/TryGhost/Team/issues/510 --- lib/limit-service.js | 18 +++++++++-------- lib/limit.js | 46 +++++++++++++++++++++++++++++++++++++++++++- test/limit.test.js | 34 +++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index 76bd3f5c91a..bd8652c8671 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -1,4 +1,4 @@ -const {MaxLimit, FlagLimit} = require('./limit'); +const {MaxLimit, FlagLimit, AllowlistLimit} = require('./limit'); const config = require('./config'); const _ = require('lodash'); @@ -30,7 +30,9 @@ class LimitService { /** @type LimitConfig */ let limitConfig = Object.assign({}, config[name], limits[name]); - if (_.has(limitConfig, 'max')) { + if (_.has(limitConfig, 'allowlist')) { + this.limits[name] = new AllowlistLimit({name, config: limitConfig, helpLink, errors}); + } else if (_.has(limitConfig, 'max')) { this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db, errors}); } else { this.limits[name] = new FlagLimit({name: name, config: limitConfig, helpLink, errors}); @@ -58,13 +60,13 @@ class LimitService { } } - async checkWouldGoOverLimit(limitName) { + async checkWouldGoOverLimit(limitName, metadata = {}) { if (!this.isLimited(limitName)) { return; } try { - await this.limits[limitName].errorIfWouldGoOverLimit(); + await this.limits[limitName].errorIfWouldGoOverLimit(metadata); return false; } catch (error) { if (error instanceof this.errors.HostLimitError) { @@ -73,20 +75,20 @@ class LimitService { } } - async errorIfIsOverLimit(limitName) { + async errorIfIsOverLimit(limitName, metadata = {}) { if (!this.isLimited(limitName)) { return; } - await this.limits[limitName].errorIfIsOverLimit(); + await this.limits[limitName].errorIfIsOverLimit(metadata); } - async errorIfWouldGoOverLimit(limitName) { + async errorIfWouldGoOverLimit(limitName, metadata = {}) { if (!this.isLimited(limitName)) { return; } - await this.limits[limitName].errorIfWouldGoOverLimit(); + await this.limits[limitName].errorIfWouldGoOverLimit(metadata); } } diff --git a/lib/limit.js b/lib/limit.js index 034a373bd14..18ae7701aef 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -157,7 +157,51 @@ class FlagLimit extends Limit { } } +class AllowlistLimit extends Limit { + constructor({name, config, helpLink, errors}) { + super({name, error: config.error || '', helpLink, errors}); + + if (!config.allowlist || !config.allowlist.length) { + throw new this.errors.IncorrectUsageError('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.value) { + throw new this.errors.IncorrectUsageError('Attempted to check an allowlist limit without a value'); + } + if (!this.allowlist.includes(metadata.value)) { + throw this.generateError(); + } + } + + async errorIfIsOverLimit(metadata) { + if (!metadata.value) { + throw new this.errors.IncorrectUsageError('Attempted to check an allowlist limit without a value'); + } + if (!this.allowlist.includes(metadata.value)) { + throw this.generateError(); + } + } +} + module.exports = { MaxLimit, - FlagLimit + FlagLimit, + AllowlistLimit }; diff --git a/test/limit.test.js b/test/limit.test.js index 9e5629d15fa..1a334a0fea7 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -3,7 +3,7 @@ require('./utils'); const errors = require('./fixtures/errors'); -const {MaxLimit} = require('../lib/limit'); +const {MaxLimit, AllowlistLimit} = require('../lib/limit'); describe('Limit Service', function () { describe('Max Limit', function () { @@ -167,4 +167,36 @@ describe('Limit Service', function () { }); }); }); + + describe('Allowlist limit', function () { + it('rejects when the allowlist config isn\'t specified', async function () { + try { + new AllowlistLimit({name: 'test', config: {}, errors}); + throw new Error('Should have failed earlier...'); + } catch (error) { + error.errorType.should.equal('IncorrectUsageError'); + } + }); + + it('accept correct values', async function () { + const limit = new AllowlistLimit({name: 'test', config: { + allowlist: ['test', 'ok'] + }, errors}); + + await limit.errorIfIsOverLimit({value: 'test'}); + }); + + it('rejects unkown values', async function () { + const limit = new AllowlistLimit({name: 'test', config: { + allowlist: ['test', 'ok'] + }, errors}); + + try { + await limit.errorIfIsOverLimit({value: 'unkown value'}); + throw new Error('Should have failed earlier...'); + } catch (error) { + error.errorType.should.equal('HostLimitError'); + } + }); + }); }); From 22ba56479a4ce7ec50f809465355d3047831841d Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 9 Apr 2021 16:10:14 +1200 Subject: [PATCH 035/255] Added a not to flag limit "errorIfIsOverLimit" method refs https://github.com/TryGhost/Team/issues/510 - Flag limits are impossible to check if they are "over a limit already" as they are just that - on/off flags. Therefore it should be directly noted that the method is there to keep the "Limit" interface and not be relied upon --- lib/limit.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/limit.js b/lib/limit.js index 18ae7701aef..32de422aad4 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -150,7 +150,8 @@ class FlagLimit extends Limit { } /** - * Flag limits are on/off so we can't be over the limit + * 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; From b76dea5f4341bf7f31cc783bd9a9b180c1d4579d Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 9 Apr 2021 23:44:12 +1200 Subject: [PATCH 036/255] Added a limit reset when loadLimits called repeatedly refs https://github.com/TryGhost/Team/issues/599 - There are cases when there'a a need to reload limits with a new set of configuration. For example, when Ghost is run in a test environment is a soft reboot is done - Resetting previous value of limits avoids having conflicting state after multiple calls --- lib/limit-service.js | 7 +++++-- test/limit-service.test.js | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index bd8652c8671..c37c79d9081 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -11,18 +11,21 @@ class LimitService { * Initializes the limits based on configuration * * @param {Object} options - * @param {Object} options.limits - hash containing limit configurations keyed by limit name and containing + * @param {Object} [options.limits] - hash containing limit configurations keyed by limit name and containing * @param {String} options.helpLink - URL pointing to help resources for when limit is reached * @param {Object} options.db - knex db connection instance or other data source for the limit checks * @param {Object} options.errors - instance of errors compatible with Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) */ - loadLimits({limits, helpLink, db, errors}) { + loadLimits({limits = {}, helpLink, db, errors}) { if (!errors) { throw new Error(`Config Missing: 'errors' is required.`); } this.errors = errors; + // CASE: reset internal limits state in case load is called multiple times + this.limits = {}; + Object.keys(limits).forEach((name) => { name = _.camelCase(name); diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 282c625901d..c0891d1dc9c 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -139,6 +139,28 @@ describe('Limit Service', function () { limitService.isLimited('custom_themes').should.be.false(); limitService.isLimited('customThemes').should.be.false(); }); + + it('populates new limits if called multiple times', function () { + const limitService = new LimitService(); + + const staffLimit = {staff: {max: 2}}; + + limitService.loadLimits({limits: staffLimit, errors}); + + limitService.limits.should.be.an.Object().with.properties(['staff']); + limitService.limits.staff.should.be.an.instanceOf(MaxLimit); + limitService.isLimited('staff').should.be.true(); + limitService.isLimited('members').should.be.false(); + + const membersLimit = {members: {max: 3}}; + + limitService.loadLimits({limits: membersLimit, errors}); + + limitService.limits.should.be.an.Object().with.properties(['members']); + limitService.limits.members.should.be.an.instanceOf(MaxLimit); + limitService.isLimited('staff').should.be.false(); + limitService.isLimited('members').should.be.true(); + }); }); describe('Custom limit count query configuration', function () { From 8fff6b36c1a24a2219ce2bfd36274132e6a0f878 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 16 Apr 2021 13:06:54 +0100 Subject: [PATCH 037/255] Unpinned all dependencies no issue - this Utils repo contains libraries, whose dependencies should not be pinned in order to reduce multiple versions of the same package appearing for consumers --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b825983639a..68e8fc22385 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "access": "public" }, "devDependencies": { - "mocha": "8.3.2", - "should": "13.2.3", - "sinon": "10.0.0" + "mocha": "^8.3.2", + "should": "^13.2.3", + "sinon": "^10.0.0" }, "dependencies": { "lodash": "^4.17.21" From 29205a97e394c364927c92ff2b75a8cecad3de3d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 16 Apr 2021 12:28:10 +0000 Subject: [PATCH 038/255] Pin dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 68e8fc22385..b825983639a 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "access": "public" }, "devDependencies": { - "mocha": "^8.3.2", - "should": "^13.2.3", - "sinon": "^10.0.0" + "mocha": "8.3.2", + "should": "13.2.3", + "sinon": "10.0.0" }, "dependencies": { "lodash": "^4.17.21" From 6062a5ebe5aac997341c02ff6ac009e6b798478b Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 9 Apr 2021 23:46:21 +1200 Subject: [PATCH 039/255] Published new versions - @tryghost/adapter-manager@0.2.11 - @tryghost/job-manager@0.8.3 - @tryghost/limit-service@0.4.1 - @tryghost/moleculer-service-from-class@0.2.14 - @tryghost/mw-session-from-token@0.1.18 - @tryghost/session-service@0.1.19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b825983639a..d00f4c24cd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.4.0", + "version": "0.4.1", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From ca0b66ecc815f1fa2efd5b9f9cad9bea7fe9f1ee Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 19 Apr 2021 10:25:57 +0100 Subject: [PATCH 040/255] Published new versions - @tryghost/adapter-manager@0.2.12 - @tryghost/errors@0.2.11 - @tryghost/image-transform@1.0.11 - @tryghost/job-manager@0.8.4 - @tryghost/limit-service@0.4.2 - @tryghost/moleculer-service-from-class@0.2.15 - @tryghost/mw-session-from-token@0.1.19 - @tryghost/pretty-cli@1.2.17 - @tryghost/promise@0.1.8 - @tryghost/release-utils@0.6.14 - @tryghost/security@0.2.8 - @tryghost/session-service@0.1.20 - @tryghost/zip@1.1.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d00f4c24cd1..498afa12e70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.4.1", + "version": "0.4.2", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From d41410a611e04cae7aee71630d638f06a08e678c Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 3 May 2021 11:07:57 +0400 Subject: [PATCH 041/255] Added notes about how config module works no issue - I was a little confused seeing an empty object in the config moduele - `customThemes: {}` and initially thought we could get rid of it to reduce the amount of code. Afte quick dig found out that there's a purpuse behind it being there! It's an allowlist of the properites that can be defined within the limit service - Added notes to clarify the usecase and avoid ambiguity in the future --- lib/config.js | 6 ++++++ lib/limit-service.js | 1 + 2 files changed, 7 insertions(+) diff --git a/lib/config.js b/lib/config.js index 51171344afe..5ff1484c6f5 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,3 +1,9 @@ +const {getMonthStart} = require('./date-utils'); + +// NOTE: to support a new config in the limit service add an empty key-object pair in the export below. +// Each type of limit has it's own structure: +// 1. FlagLimit and AllowlistLimit types are empty objects paired with a key, e.g.: `customThemes: {}` +// 2. MaxLimit should contain a `currentCountQuery` function which would count the resources under limit module.exports = { members: { currentCountQuery: async (db) => { diff --git a/lib/limit-service.js b/lib/limit-service.js index c37c79d9081..c34a3a7d939 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -29,6 +29,7 @@ class LimitService { Object.keys(limits).forEach((name) => { name = _.camelCase(name); + // NOTE: config module acts as an allowlist of supported config names, where each key is a name of supported config if (config[name]) { /** @type LimitConfig */ let limitConfig = Object.assign({}, config[name], limits[name]); From e6fab25f264607c0a9b1077afb71d93f5508f077 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 3 May 2021 11:11:23 +0400 Subject: [PATCH 042/255] Fixed failing build refs https://github.com/TryGhost/Utills/commit/8a057ea655579303e8a8c3d7adbb1edbcb85c504 - Had a stray code commited in refed commit --- lib/config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/config.js b/lib/config.js index 5ff1484c6f5..2daba4b126e 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,5 +1,3 @@ -const {getMonthStart} = require('./date-utils'); - // NOTE: to support a new config in the limit service add an empty key-object pair in the export below. // Each type of limit has it's own structure: // 1. FlagLimit and AllowlistLimit types are empty objects paired with a key, e.g.: `customThemes: {}` From a334722a8c427c9c1dbfddb8c718d5e04522b55d Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 3 May 2021 11:47:55 +0400 Subject: [PATCH 043/255] Added flag limit support for "emails" refs https://github.com/TryGhost/Team/issues/588 - This is a step 1 in the introduction of email limits. Next step would be allowing this limit to support "periodical limit checks" --- lib/config.js | 1 + test/limit-service.test.js | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index 2daba4b126e..37031f5be02 100644 --- a/lib/config.js +++ b/lib/config.js @@ -9,6 +9,7 @@ module.exports = { return result.count; } }, + emails: {}, staff: { currentCountQuery: async (db) => { let result = await db.knex('users') diff --git a/test/limit-service.test.js b/test/limit-service.test.js index c0891d1dc9c..b9f3f1de7a6 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -86,7 +86,11 @@ describe('Limit Service', function () { it('can load multiple limits', function () { const limitService = new LimitService(); - let limits = {staff: {max: 2}, members: {max: 100}}; + let limits = { + staff: {max: 2}, + members: {max: 100}, + emails: {disabled: true} + }; limitService.loadLimits({limits, errors}); @@ -95,6 +99,7 @@ describe('Limit Service', function () { limitService.limits.members.should.be.an.instanceOf(MaxLimit); limitService.isLimited('staff').should.be.true(); limitService.isLimited('members').should.be.true(); + limitService.isLimited('emails').should.be.true(); }); it('can load camel cased limits', function () { @@ -138,6 +143,7 @@ describe('Limit Service', function () { limitService.isLimited('members').should.be.false(); limitService.isLimited('custom_themes').should.be.false(); limitService.isLimited('customThemes').should.be.false(); + limitService.isLimited('emails').should.be.false(); }); it('populates new limits if called multiple times', function () { From 45e714447120d80fc2cea53d1bd078bef430156c Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 3 May 2021 12:02:01 +0400 Subject: [PATCH 044/255] Added test coverage for flag type of limits refs https://github.com/TryGhost/Team/issues/588 - This is by no means an thorought test coverage but ensures the basics work and provides examples of how the limit should be used. To be continued :) --- test/limit.test.js | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/test/limit.test.js b/test/limit.test.js index 1a334a0fea7..b2b18a83119 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -3,9 +3,46 @@ require('./utils'); const errors = require('./fixtures/errors'); -const {MaxLimit, AllowlistLimit} = require('../lib/limit'); +const {MaxLimit, AllowlistLimit, FlagLimit} = require('../lib/limit'); describe('Limit Service', function () { + describe('Flag Limit', function () { + it('do nothing if is over limit', async function () { + // NOTE: the behavior of flag limit in "is over limit" usecase is flawed and should not be relied on + // possible solution could be throwing an error to prevent clients from using it? + const config = { + disabled: true + }; + const limit = new FlagLimit({name: 'flaggy', config, errors}); + + const result = await limit.errorIfIsOverLimit(); + should(result).be.undefined(); + }); + + it('throws if would go over limit', async function () { + const config = { + disabled: true + }; + const limit = new FlagLimit({name: 'flaggy', config, errors}); + + try { + await limit.errorIfWouldGoOverLimit(); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); + + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'flaggy'); + + should.exist(err.message); + should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.'); + } + }); + }); + describe('Max Limit', function () { describe('Constructor', function () { it('throws if initialized without a max limit', function () { From cc7a3e2f1f1c45555f8422d462b06efec6f9aae9 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 3 May 2021 12:05:01 +0400 Subject: [PATCH 045/255] Published new versions - @tryghost/limit-service@0.4.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 498afa12e70..453e86cc006 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.4.2", + "version": "0.4.3", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From d4455caf6c07fd0ef2610ddf59e9dcdef222e52e Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 5 May 2021 12:42:30 +0400 Subject: [PATCH 046/255] Fixed typos --- test/limit.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/limit.test.js b/test/limit.test.js index b2b18a83119..a4906c8f877 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -223,13 +223,13 @@ describe('Limit Service', function () { await limit.errorIfIsOverLimit({value: 'test'}); }); - it('rejects unkown values', async function () { + it('rejects unknown values', async function () { const limit = new AllowlistLimit({name: 'test', config: { allowlist: ['test', 'ok'] }, errors}); try { - await limit.errorIfIsOverLimit({value: 'unkown value'}); + await limit.errorIfIsOverLimit({value: 'unknown value'}); throw new Error('Should have failed earlier...'); } catch (error) { error.errorType.should.equal('HostLimitError'); From 464de405d7c3cc0887b36704ba5e694ccc61685a Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 6 May 2021 14:16:22 +0400 Subject: [PATCH 047/255] Added "maxPeriodic" limit type refs https://github.com/TryGhost/Team/issues/588 - This is a scaffolding for a new limit type which should allow to check limits based on periods (for example related to billing, subscription cycles) --- lib/date-utils.js | 5 +++ lib/limit.js | 48 +++++++++++++++++++++++++ test/limit.test.js | 89 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 lib/date-utils.js diff --git a/lib/date-utils.js b/lib/date-utils.js new file mode 100644 index 00000000000..a6716e1e1bd --- /dev/null +++ b/lib/date-utils.js @@ -0,0 +1,5 @@ +const SUPPORTED_INTERVALS = ['month']; + +module.exports = { + SUPPORTED_INTERVALS +}; diff --git a/lib/limit.js b/lib/limit.js index 32de422aad4..e60710797f8 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -1,5 +1,7 @@ // run in context allows us to change the templateSettings without causing havoc const _ = require('lodash').runInContext(); +const {SUPPORTED_INTERVALS} = require('./date-utils'); + _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; class Limit { @@ -110,6 +112,51 @@ class MaxLimit extends Limit { } } +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.`; + } +} + class FlagLimit extends Limit { /** * @@ -203,6 +250,7 @@ class AllowlistLimit extends Limit { module.exports = { MaxLimit, + MaxPeriodicLimit, FlagLimit, AllowlistLimit }; diff --git a/test/limit.test.js b/test/limit.test.js index a4906c8f877..a2e53806d7a 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -3,7 +3,7 @@ require('./utils'); const errors = require('./fixtures/errors'); -const {MaxLimit, AllowlistLimit, FlagLimit} = require('../lib/limit'); +const {MaxLimit, AllowlistLimit, FlagLimit, MaxPeriodicLimit} = require('../lib/limit'); describe('Limit Service', function () { describe('Flag Limit', function () { @@ -205,6 +205,93 @@ describe('Limit Service', function () { }); }); + describe('Periodic Max Limit', function () { + describe('Constructor', function () { + it('throws if initialized without a maxPeriodic limit', function () { + const config = {}; + + try { + const limit = new MaxPeriodicLimit({name: 'no limits!', config, errors}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + err.message.should.match(/periodic max limit without a limit/gi); + } + }); + + it('throws if initialized without a current count query', function () { + const config = { + maxPeriodic: 100 + }; + + try { + const limit = new MaxPeriodicLimit({name: 'no accountability!', config, errors}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + err.message.should.match(/periodic max limit without a current count query/gi); + } + }); + + it('throws if initialized without interval', function () { + const config = { + maxPeriodic: 100, + currentCountQuery: () => {} + }; + + try { + const limit = new MaxPeriodicLimit({name: 'no accountability!', config, errors}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + err.message.should.match(/periodic max limit without an interval/gi); + } + }); + + it('throws if initialized with unsupported interval', function () { + const config = { + maxPeriodic: 100, + currentCountQuery: () => {}, + interval: 'week' + }; + + try { + const limit = new MaxPeriodicLimit({name: 'no accountability!', config, errors}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + err.message.should.match(/periodic max limit without unsupported interval. Please specify one of: month/gi); + } + }); + + it('throws if initialized without start date', function () { + const config = { + maxPeriodic: 100, + currentCountQuery: () => {}, + interval: 'month' + }; + + try { + const limit = new MaxPeriodicLimit({name: 'no accountability!', config, errors}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'IncorrectUsageError'); + err.message.should.match(/periodic max limit without a start date/gi); + } + }); + }); + }); + describe('Allowlist limit', function () { it('rejects when the allowlist config isn\'t specified', async function () { try { From 996fb0ce707c9f01d3363c75cb98c848c77facba Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 6 May 2021 15:19:36 +0400 Subject: [PATCH 048/255] Added utility calculating date of last period start refs https://github.com/TryGhost/Team/issues/588 - There's a need to calculate when the last period has started to be able to generate correct counting queries for the "maxPeriodical" limit - It operatest on ISO strings as an input and output in UTC timezone to take timezone calculations out of the equation - Refer to inclucded unit tests for example calculations --- lib/date-utils.js | 25 ++++++++++++++ package.json | 1 + test/date-utils.test.js | 76 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 test/date-utils.test.js diff --git a/lib/date-utils.js b/lib/date-utils.js index a6716e1e1bd..f27da80eb72 100644 --- a/lib/date-utils.js +++ b/lib/date-utils.js @@ -1,5 +1,30 @@ +const differenceInMonths = require('date-fns/differenceInMonths'); +const parseISO = require('date-fns/parseISO'); +const addMonths = require('date-fns/addMonths'); + const SUPPORTED_INTERVALS = ['month']; +/** + * Calculates the start of the last period (billing, cycle, etc.) based on the start date + * and the interval at which the cycle renews. + * + * @param {String} startDate - date in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601) + * @param {('month')} interval - currently only supports 'month' value, in the future might support 'year', etc. + * + * @returns {String} - date in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601) of the last period start + */ +const lastPeriodStart = (startDate, interval) => { + if (interval === 'month') { + const startDateISO = parseISO(startDate); + const fullPeriodsPast = differenceInMonths(new Date(), startDateISO); + const lastPeriodStartDate = addMonths(startDateISO, fullPeriodsPast); + + return lastPeriodStartDate.toISOString(); + } + + throw new Error('Invalid interval specified. Only "month" value is accepted.'); +}; module.exports = { + lastPeriodStart, SUPPORTED_INTERVALS }; diff --git a/package.json b/package.json index 453e86cc006..c1faf51ef3d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "access": "public" }, "devDependencies": { + "date-fns": "^2.21.2", "mocha": "8.3.2", "should": "13.2.3", "sinon": "10.0.0" diff --git a/test/date-utils.test.js b/test/date-utils.test.js new file mode 100644 index 00000000000..57d3286aaf3 --- /dev/null +++ b/test/date-utils.test.js @@ -0,0 +1,76 @@ +// Switch these lines once there are useful utils +// const testUtils = require('./utils'); +require('./utils'); + +const {subWeeks, subMonths} = require('date-fns'); +const sinon = require('sinon'); +const {lastPeriodStart} = require('../lib/date-utils'); + +describe('Date Utils', function () { + describe('fn: lastPeriodStart', function () { + let clock; + + afterEach(function () { + if (clock) { + clock.restore(); + } + }); + + it('returns same date if current date is less than a period away from current date', async function () { + const weekAgoDate = subWeeks(new Date(), 1); + const weekAgoISO = weekAgoDate.toISOString(); + + const lastPeriodStartDate = lastPeriodStart(weekAgoISO, 'month'); + + lastPeriodStartDate.should.equal(weekAgoISO); + }); + + it('returns beginning of last month\'s period', async function () { + const weekAgoDate = subWeeks(new Date(), 1); + const weekAgoISO = weekAgoDate.toISOString(); + + const weekAndAMonthAgo = subMonths(weekAgoDate, 1); + const weekAndAMonthAgoISO = weekAndAMonthAgo.toISOString(); + + const lastPeriodStartDate = lastPeriodStart(weekAndAMonthAgoISO, 'month'); + + lastPeriodStartDate.should.equal(weekAgoISO); + }); + + it('returns 3rd day or current month when monthly period started on 3rd day in the past', async function () { + // fake current clock to be past 3rd day of a month + clock = sinon.useFakeTimers(new Date('2021-08-18T19:00:52Z').getTime()); + + const lastPeriodStartDate = lastPeriodStart('2020-03-03T23:00:01Z', 'month'); + + lastPeriodStartDate.should.equal('2021-08-03T23:00:01.000Z'); + }); + + it('returns 5rd day or last month when monthly period started on 5th day in the past and it is 3rd day of the month', async function () { + // fake current clock to be on 3rd day of a month + clock = sinon.useFakeTimers(new Date('2021-09-03T12:12:12Z').getTime()); + + const lastPeriodStartDate = lastPeriodStart('2020-03-05T11:11:11Z', 'month'); + + lastPeriodStartDate.should.equal('2021-08-05T11:11:11.000Z'); + }); + + it('return 29th of Feb if the subscription started on the 31st day and it is a leap year', async function () { + // fake current clock to be march of a leap year + clock = sinon.useFakeTimers(new Date('2020-03-05T13:15:07Z').getTime()); + + const lastPeriodStartDate = lastPeriodStart('2020-01-31T23:00:01Z', 'month'); + + lastPeriodStartDate.should.equal('2020-02-29T23:00:01.000Z'); + }); + + it('return 28th of Feb if the subscription started on the 30th day and it is **not** a leap year', async function () { + // fake current clock to be March of non-leap year + clock = sinon.useFakeTimers(new Date('2021-03-05T13:15:07Z').getTime()); + + const lastPeriodStartDate = lastPeriodStart('2019-04-30T01:59:42Z', 'month'); + + lastPeriodStartDate.should.equal('2021-02-28T01:59:42.000Z'); + }); + }); +}); From 6f2c037b5d5497caad959c17d5f7b1d1d298010f Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 6 May 2021 15:42:02 +0400 Subject: [PATCH 049/255] Fixed IncorrectUsageError initialization no issue - The error takes in an options object which should contain "message" property instead of a string --- lib/limit.js | 10 +++++----- test/limit.test.js | 7 ++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index e60710797f8..0bd96de8396 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -44,11 +44,11 @@ class MaxLimit extends Limit { super({name, error: config.error || '', helpLink, db, errors}); if (config.max === undefined) { - throw new errors.IncorrectUsageError('Attempted to setup a max limit without a limit'); + throw new errors.IncorrectUsageError({message: 'Attempted to setup a max limit without a limit'}); } if (!config.currentCountQuery) { - throw new errors.IncorrectUsageError('Attempted to setup a max limit without a current count query'); + throw new errors.IncorrectUsageError({message: 'Attempted to setup a max limit without a current count query'}); } this.currentCountQueryFn = config.currentCountQuery; @@ -210,7 +210,7 @@ class AllowlistLimit extends Limit { super({name, error: config.error || '', helpLink, errors}); if (!config.allowlist || !config.allowlist.length) { - throw new this.errors.IncorrectUsageError('Attempted to setup an allowlist limit without an allowlist'); + throw new this.errors.IncorrectUsageError({message: 'Attempted to setup an allowlist limit without an allowlist'}); } this.allowlist = config.allowlist; @@ -231,7 +231,7 @@ class AllowlistLimit extends Limit { async errorIfWouldGoOverLimit(metadata) { if (!metadata.value) { - throw new this.errors.IncorrectUsageError('Attempted to check an allowlist limit without a 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(); @@ -240,7 +240,7 @@ class AllowlistLimit extends Limit { async errorIfIsOverLimit(metadata) { if (!metadata.value) { - throw new this.errors.IncorrectUsageError('Attempted to check an allowlist limit without a 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(); diff --git a/test/limit.test.js b/test/limit.test.js index a2e53806d7a..0214291143d 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -55,11 +55,14 @@ describe('Limit Service', function () { should.exist(err); should.exist(err.errorType); should.equal(err.errorType, 'IncorrectUsageError'); + err.message.should.match(/max limit without a limit/); } }); it('throws if initialized without a current count query', function () { - const config = {}; + const config = { + max: 100 + }; try { const limit = new MaxLimit({name: 'no accountability!', config, errors}); @@ -68,6 +71,7 @@ describe('Limit Service', function () { should.exist(err); should.exist(err.errorType); should.equal(err.errorType, 'IncorrectUsageError'); + err.message.should.match(/max limit without a current count query/); } }); }); @@ -299,6 +303,7 @@ describe('Limit Service', function () { throw new Error('Should have failed earlier...'); } catch (error) { error.errorType.should.equal('IncorrectUsageError'); + error.message.should.match(/allowlist limit without an allowlist/); } }); From 15f59a90025495c0e4b2ab264bb81138540617cf Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 6 May 2021 15:49:16 +0400 Subject: [PATCH 050/255] Added maxPeriodic limit support to limit service refs https://github.com/TryGhost/Team/issues/588 - The limit service can now be initialized with a config which has a 'maxPeriodic' key identifying it's a special type of limit taking subscription cycles into account - Example configuration can be found in the included unit tests --- lib/limit-service.js | 13 ++++++++++-- test/limit-service.test.js | 42 +++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index c34a3a7d939..c2f8b242699 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -1,4 +1,4 @@ -const {MaxLimit, FlagLimit, AllowlistLimit} = require('./limit'); +const {MaxLimit, MaxPeriodicLimit, FlagLimit, AllowlistLimit} = require('./limit'); const config = require('./config'); const _ = require('lodash'); @@ -12,11 +12,12 @@ class LimitService { * * @param {Object} options * @param {Object} [options.limits] - hash containing limit configurations keyed by limit name and containing + * @param {Object} [options.subscription] - hash containing subscription configuration with interval and startDate properties * @param {String} options.helpLink - URL pointing to help resources for when limit is reached * @param {Object} options.db - knex db connection instance or other data source for the limit checks * @param {Object} options.errors - instance of errors compatible with Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) */ - loadLimits({limits = {}, helpLink, db, errors}) { + loadLimits({limits = {}, subscription, helpLink, db, errors}) { if (!errors) { throw new Error(`Config Missing: 'errors' is required.`); } @@ -38,6 +39,13 @@ class LimitService { this.limits[name] = new AllowlistLimit({name, config: limitConfig, helpLink, errors}); } else if (_.has(limitConfig, 'max')) { this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db, errors}); + } else if (_.has(limitConfig, 'maxPeriodic')) { + if (subscription === undefined) { + throw new errors.IncorrectUsageError({message: 'Attempted to setup a periodic max limit without a subscription'}); + } + + const maxPeriodicLimitConfig = Object.assign({}, limitConfig, subscription); + this.limits[name] = new MaxPeriodicLimit({name: name, config: maxPeriodicLimitConfig, helpLink, db, errors}); } else { this.limits[name] = new FlagLimit({name: name, config: limitConfig, helpLink, errors}); } @@ -101,6 +109,7 @@ module.exports = LimitService; /** * @typedef {Object} LimitConfig * @prop {Number} [max] - max limit + * @prop {Number} [maxPeriodic] - max limit for a period * @prop {Boolean} [disabled] - flag disabling/enabling limit * @prop {String} error - custom error to be displayed when the limit is reached * @prop {Function} [currentCountQuery] - function returning count for the "max" type of limit diff --git a/test/limit-service.test.js b/test/limit-service.test.js index b9f3f1de7a6..1e8f731e34e 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -3,7 +3,7 @@ require('./utils'); const LimitService = require('../lib/limit-service'); -const {MaxLimit, FlagLimit} = require('../lib/limit'); +const {MaxLimit, MaxPeriodicLimit, FlagLimit} = require('../lib/limit'); const errors = require('./fixtures/errors'); @@ -83,6 +83,46 @@ describe('Limit Service', function () { limitService.isLimited('members').should.be.false(); }); + it('can load a periodic max limit', function () { + const limitService = new LimitService(); + + let limits = { + emails: { + maxPeriodic: 3 + } + }; + + let subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, subscription, errors}); + + limitService.limits.should.be.an.Object().with.properties(['emails']); + limitService.limits.emails.should.be.an.instanceOf(MaxPeriodicLimit); + limitService.isLimited('emails').should.be.true(); + limitService.isLimited('staff').should.be.false(); + }); + + it('throws when loadding a periodic max limit without a subscription', function () { + const limitService = new LimitService(); + + let limits = { + emails: { + maxPeriodic: 3 + } + }; + + try { + limitService.loadLimits({limits, errors}); + throw new Error('Should have failed earlier...'); + } catch (error) { + error.errorType.should.equal('IncorrectUsageError'); + error.message.should.match(/periodic max limit without a subscription/); + } + }); + it('can load multiple limits', function () { const limitService = new LimitService(); From e7e0777aa77241acdd231a34e3da4100da75403b Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 6 May 2021 17:37:50 +0400 Subject: [PATCH 051/255] Added currentCountQuery for emails limit refs https://github.com/TryGhost/Team/issues/588 - This is a basic implementation which needs a review. Implemented it to fix failing tests in main - Start date is expected to come formatted for DB's needs --- lib/config.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index 37031f5be02..0cb58d842a3 100644 --- a/lib/config.js +++ b/lib/config.js @@ -9,7 +9,16 @@ module.exports = { return result.count; } }, - emails: {}, + emails: { + currentCountQuery: async (db, startDate) => { + let result = await db.knex('emails') + .count('id', {as: 'count'}) + .where('created_at', '>=', startDate) + .first(); + + return result.count; + } + }, staff: { currentCountQuery: async (db) => { let result = await db.knex('users') From 7e4ab0057155705e2312b5080c4132d80c02b2d8 Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 6 May 2021 17:48:31 +0400 Subject: [PATCH 052/255] Added added maxPeriodical checks refs https://github.com/TryGhost/Team/issues/588 - This bit is putting together all the pieces for periodical limit checks. More tests are to come --- lib/limit.js | 59 +++++++++++++++++++++++++++++++++++++++++++++- test/limit.test.js | 36 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/lib/limit.js b/lib/limit.js index 0bd96de8396..15af078840f 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -1,6 +1,6 @@ // run in context allows us to change the templateSettings without causing havoc const _ = require('lodash').runInContext(); -const {SUPPORTED_INTERVALS} = require('./date-utils'); +const {lastPeriodStart, SUPPORTED_INTERVALS} = require('./date-utils'); _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; @@ -155,6 +155,63 @@ class MaxPeriodicLimit extends Limit { 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) + }); + } 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 + */ + async errorIfWouldGoOverLimit({max} = {}) { + let currentCount = await this.currentCountQuery(this.db); + + if ((currentCount + 1) > (max || this.maxPeriodic)) { + 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); + } + } } class FlagLimit extends Limit { diff --git a/test/limit.test.js b/test/limit.test.js index 0214291143d..0b3b4d20c8f 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -294,6 +294,42 @@ describe('Limit Service', function () { } }); }); + + describe('Is over limit', function () { + it('throws if is over the limit', async function () { + const currentCountyQueryMock = sinon.mock().returns(11); + + const config = { + maxPeriodic: 3, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: currentCountyQueryMock + }; + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, errors}); + await limit.errorIfIsOverLimit(); + } catch (error) { + error.errorType.should.equal('HostLimitError'); + error.errorDetails.name.should.equal('mailguard'); + error.errorDetails.limit.should.equal(3); + error.errorDetails.total.should.equal(11); + + currentCountyQueryMock.callCount.should.equal(1); + should(currentCountyQueryMock.args).not.be.undefined(); + should(currentCountyQueryMock.args[0][0]).be.undefined(); //knex db connection + + const nowDate = new Date(); + const startOfTheMonthDate = new Date(Date.UTC( + nowDate.getUTCFullYear(), + nowDate.getUTCMonth() + )).toISOString(); + + currentCountyQueryMock.args[0][1].should.equal(startOfTheMonthDate); + } + }); + }); }); describe('Allowlist limit', function () { From 3e10ee507fefab0e891cae011658f8017167af0e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 6 May 2021 14:29:55 +0000 Subject: [PATCH 053/255] Pin dependency date-fns to 2.21.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1faf51ef3d..7adf0681cbb 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "date-fns": "^2.21.2", + "date-fns": "2.21.2", "mocha": "8.3.2", "should": "13.2.3", "sinon": "10.0.0" From c55701a5f690e0b8d68d4da3b788d452602bc52e Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 7 May 2021 11:46:17 +0400 Subject: [PATCH 054/255] Clarified test name --- test/limit-service.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 1e8f731e34e..1f22850f55a 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -70,7 +70,7 @@ describe('Limit Service', function () { } }); - it('can load a basic limit', function () { + it('can load a max limit', function () { const limitService = new LimitService(); let limits = {staff: {max: 2}}; From a7330ad737d30232d3584d5b28151e06de50af24 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 7 May 2021 14:41:52 +0400 Subject: [PATCH 055/255] Fixed time difference calculation in DST timezones refs https://github.com/TryGhost/Team/issues/588 - date-fns proved to be unable to manipulate dates consistently in UTC timezone. Keeping all calculations and formatting in UTC is key to have consistency in dates when dealing in inter-system dates - day.js also failed the test for correct UTC manipulation. See https://github.com/iamkun/dayjs/issues/1271 for example bug which prevents from consistent correct calculation - luxon was the best option which WORKED. It's also a recommended successor for moment.js with really nice docs and active support --- lib/date-utils.js | 14 +++++++------- package.json | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/date-utils.js b/lib/date-utils.js index f27da80eb72..7e1943137bf 100644 --- a/lib/date-utils.js +++ b/lib/date-utils.js @@ -1,6 +1,4 @@ -const differenceInMonths = require('date-fns/differenceInMonths'); -const parseISO = require('date-fns/parseISO'); -const addMonths = require('date-fns/addMonths'); +const {DateTime} = require('luxon'); const SUPPORTED_INTERVALS = ['month']; /** @@ -14,11 +12,13 @@ const SUPPORTED_INTERVALS = ['month']; */ const lastPeriodStart = (startDate, interval) => { if (interval === 'month') { - const startDateISO = parseISO(startDate); - const fullPeriodsPast = differenceInMonths(new Date(), startDateISO); - const lastPeriodStartDate = addMonths(startDateISO, fullPeriodsPast); + const startDateISO = DateTime.fromISO(startDate, {zone: 'UTC'}); + const now = DateTime.now().setZone('UTC'); + const fullPeriodsPast = Math.floor(now.diff(startDateISO, 'months').months); - return lastPeriodStartDate.toISOString(); + const lastPeriodStartDate = startDateISO.plus({months: fullPeriodsPast}); + + return lastPeriodStartDate.toISO(); } throw new Error('Invalid interval specified. Only "month" value is accepted.'); diff --git a/package.json b/package.json index 7adf0681cbb..76130d0df38 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "sinon": "10.0.0" }, "dependencies": { - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "luxon": "^1.26.0" } } From 417f13ef5c1b234ce9ffb881835f2676b4c740b6 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 7 May 2021 14:56:40 +0400 Subject: [PATCH 056/255] Removed date-fns dev dependency refs https://github.com/TryGhost/Team/issues/588 refs https://github.com/TryGhost/Utils/commit/e9f1cfcf6d7fe05ef388b0594c9010ec7e9b334a - date-fns proved to be unable to manipulate dates in consistent UTC format and was substitured with luxon in referenced commit. Removing it from tests for consistency --- package.json | 1 - test/date-utils.test.js | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 76130d0df38..9421ee6fd24 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "access": "public" }, "devDependencies": { - "date-fns": "2.21.2", "mocha": "8.3.2", "should": "13.2.3", "sinon": "10.0.0" diff --git a/test/date-utils.test.js b/test/date-utils.test.js index 57d3286aaf3..e0585e6b0e6 100644 --- a/test/date-utils.test.js +++ b/test/date-utils.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('./utils'); -const {subWeeks, subMonths} = require('date-fns'); +const {DateTime} = require('luxon'); const sinon = require('sinon'); const {lastPeriodStart} = require('../lib/date-utils'); @@ -17,8 +17,8 @@ describe('Date Utils', function () { }); it('returns same date if current date is less than a period away from current date', async function () { - const weekAgoDate = subWeeks(new Date(), 1); - const weekAgoISO = weekAgoDate.toISOString(); + const weekAgoDate = DateTime.now().toUTC().plus({weeks: -1}); + const weekAgoISO = weekAgoDate.toISO(); const lastPeriodStartDate = lastPeriodStart(weekAgoISO, 'month'); @@ -26,11 +26,11 @@ describe('Date Utils', function () { }); it('returns beginning of last month\'s period', async function () { - const weekAgoDate = subWeeks(new Date(), 1); - const weekAgoISO = weekAgoDate.toISOString(); + const weekAgoDate = DateTime.now().toUTC().plus({weeks: -1}); + const weekAgoISO = weekAgoDate.toISO(); - const weekAndAMonthAgo = subMonths(weekAgoDate, 1); - const weekAndAMonthAgoISO = weekAndAMonthAgo.toISOString(); + const weekAndAMonthAgo = weekAgoDate.plus({months: -1}); + const weekAndAMonthAgoISO = weekAndAMonthAgo.toISO(); const lastPeriodStartDate = lastPeriodStart(weekAndAMonthAgoISO, 'month'); From 682536fdb94efab5940b4010ae0955776e38fb58 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 7 May 2021 15:00:07 +0400 Subject: [PATCH 057/255] Published new versions - @tryghost/job-manager@0.8.5 - @tryghost/limit-service@0.4.4 - @tryghost/mw-session-from-token@0.1.20 - @tryghost/package-json@0.1.0 - @tryghost/session-service@0.1.21 - @tryghost/zip@1.1.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9421ee6fd24..d57df346ed1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.4.3", + "version": "0.4.4", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 307c8aab81cb0d1c6ce1de21c42b86110fe7f187 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 7 May 2021 17:56:30 +0400 Subject: [PATCH 058/255] Fixed query counting total emails sent in a period refs https://github.com/TryGhost/Team/issues/588 - The previous query was quickly copied from stats-service which was using incorrect table for the count - Updated version sums up email_count values for emails in given period of time --- lib/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index 0cb58d842a3..3e4aff0afd5 100644 --- a/lib/config.js +++ b/lib/config.js @@ -12,7 +12,7 @@ module.exports = { emails: { currentCountQuery: async (db, startDate) => { let result = await db.knex('emails') - .count('id', {as: 'count'}) + .sum('email_count', {as: 'count'}) .where('created_at', '>=', startDate) .first(); From b4efc376ec3f9f7f552e19c2c744bb24ea9b9f00 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 7 May 2021 18:13:01 +0400 Subject: [PATCH 059/255] Added addedCount to max and maxPeriodic limits refs https://github.com/TryGhost/Team/issues/588 - The `addedCount` parameter in `errorIfWouldGoOverLimit` method allows to specify a custom resource count that is about to be added. Example usecase is when we'd want to send a 100 emails and current limit is 99, and none have been sent so far. With previous implementation the check would've passed because it only checked for single resource that would be added through "+1". Current implementation allows to specify the amount of recources to be added --- lib/limit.js | 8 ++-- test/limit.test.js | 113 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index 15af078840f..7754f080cb4 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -89,10 +89,10 @@ class MaxLimit extends Limit { * @param {Object} options * @param {Number} [options.max] - overrides configured default max value to perform checks against */ - async errorIfWouldGoOverLimit({max} = {}) { + async errorIfWouldGoOverLimit({max, addedCount = 1} = {}) { let currentCount = await this.currentCountQuery(this.db); - if ((currentCount + 1) > (max || this.max)) { + if ((currentCount + addedCount) > (max || this.max)) { throw this.generateError(currentCount); } } @@ -191,10 +191,10 @@ class MaxPeriodicLimit extends Limit { * @param {Object} options * @param {Number} [options.max] - overrides configured default maxPeriodic value to perform checks against */ - async errorIfWouldGoOverLimit({max} = {}) { + async errorIfWouldGoOverLimit({max, addedCount = 1} = {}) { let currentCount = await this.currentCountQuery(this.db); - if ((currentCount + 1) > (max || this.maxPeriodic)) { + if ((currentCount + addedCount) > (max || this.maxPeriodic)) { throw this.generateError(currentCount); } } diff --git a/test/limit.test.js b/test/limit.test.js index 0b3b4d20c8f..eaaa0057fa5 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -167,6 +167,30 @@ describe('Limit Service', function () { } }); + it('throws if would go over the limit with with custom added count', async function () { + const config = { + max: 23, + currentCountQuery: () => 13 + }; + const limit = new MaxLimit({name: 'maxy', config, errors}); + + try { + await limit.errorIfWouldGoOverLimit({addedCount: 11}); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); + + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'maxy'); + + should.exist(err.message); + should.equal(err.message, 'This action would exceed the maxy limit on your current plan.'); + } + }); + it('passes if does not go over the limit', async function () { const config = { max: 2, @@ -330,6 +354,95 @@ describe('Limit Service', function () { } }); }); + + describe('Would go over limit', function () { + it('passes if within the limit', async function () { + const currentCountyQueryMock = sinon.mock().returns(4); + + const config = { + maxPeriodic: 5, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: currentCountyQueryMock + }; + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, errors}); + await limit.errorIfWouldGoOverLimit(); + } catch (error) { + should.fail('MaxPeriodicLimit errorIfWouldGoOverLimit check should not have errored'); + } + }); + + it('throws if would go over limit', async function () { + const currentCountyQueryMock = sinon.mock().returns(5); + + const config = { + maxPeriodic: 5, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: currentCountyQueryMock + }; + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, errors}); + await limit.errorIfWouldGoOverLimit(); + } catch (error) { + error.errorType.should.equal('HostLimitError'); + error.errorDetails.name.should.equal('mailguard'); + error.errorDetails.limit.should.equal(5); + error.errorDetails.total.should.equal(5); + + currentCountyQueryMock.callCount.should.equal(1); + should(currentCountyQueryMock.args).not.be.undefined(); + should(currentCountyQueryMock.args[0][0]).be.undefined(); //knex db connection + + const nowDate = new Date(); + const startOfTheMonthDate = new Date(Date.UTC( + nowDate.getUTCFullYear(), + nowDate.getUTCMonth() + )).toISOString(); + + currentCountyQueryMock.args[0][1].should.equal(startOfTheMonthDate); + } + }); + + it('throws if would go over limit with custom added count', async function () { + const currentCountyQueryMock = sinon.mock().returns(5); + + const config = { + maxPeriodic: 13, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: currentCountyQueryMock + }; + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, errors}); + await limit.errorIfWouldGoOverLimit({addedCount: 9}); + } catch (error) { + error.errorType.should.equal('HostLimitError'); + error.errorDetails.name.should.equal('mailguard'); + error.errorDetails.limit.should.equal(13); + error.errorDetails.total.should.equal(5); + + currentCountyQueryMock.callCount.should.equal(1); + should(currentCountyQueryMock.args).not.be.undefined(); + should(currentCountyQueryMock.args[0][0]).be.undefined(); //knex db connection + + const nowDate = new Date(); + const startOfTheMonthDate = new Date(Date.UTC( + nowDate.getUTCFullYear(), + nowDate.getUTCMonth() + )).toISOString(); + + currentCountyQueryMock.args[0][1].should.equal(startOfTheMonthDate); + } + }); + }); }); describe('Allowlist limit', function () { From d7e74944dbc5d883f24a19177eb7b9610a516ca5 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 7 May 2021 18:23:35 +0400 Subject: [PATCH 060/255] Published new versions - @tryghost/limit-service@0.5.0 - @tryghost/package-json@0.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d57df346ed1..910c771d1cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.4.4", + "version": "0.5.0", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 3631821cd0228a534a19a8de9584127963c3c079 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 11 May 2021 16:29:10 +0400 Subject: [PATCH 061/255] Added emails limit to documentation example refs https://github.com/TryGhost/Team/issues/588 - The "emails" limit was added with recent changes and could be configured as either "flag" or "maxPeridoci" type of limit - More docs on different types of limits to follow --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e4c5fad2ea..25c3d94d630 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,29 @@ const limits = { customIntegrations: { disabled: true, error: 'You can use all our official, built-in integrations on the Starter plan. If you upgrade to one of our higher tiers, you’ll also be able to create and edit custom integrations and API keys for advanced workflows.' - } + }, + // emails is a hybrid type of limit that can be a "flag" or a "max periodic" type + // below is a "flag" type configuration + emails: { + disabled: true, + error: 'Email sending has been temporarily disabled whilst your account is under review.' + }, + // following is a "max periodic" type of configuration + // note if you use this configuration, the limit service has to also get a + // "subscription" parameter to work as expected + // emails: { + // maxPeriodic: 42, + // error: 'Your plan supports up to {{max}} emails. Please upgrade to reenable sending emails.' + // } +}; + +// This information is needed for the limit service to work with "max periodic" limits +// The interval value has to be 'month' as thats the only interval that was needed for +// current usecase +// The startDate has to be in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601) +const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' }; // initialize the URL linking to help documentation etc. @@ -62,7 +84,7 @@ const db = knex({ }); // finish initializing the limits service -limitService.loadLimits({limits, db, helpLink, errors}); +limitService.loadLimits({limits, subscription, db, helpLink, errors}); // perform limit checks From f745d602136534745f511422d1da3ce62548211e Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 11 May 2021 16:31:34 +0400 Subject: [PATCH 062/255] Added customThemes limit to config example refs https://github.com/TryGhost/Team/issues/590 - The "allowList" type of configuration was missing from the example, added it for reference --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 25c3d94d630..173fe271413 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ const limits = { disabled: true, error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.' }, + // customThemes: { + // allowlist: ['casper', 'dawn', 'lyra'], + // error: "All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site." + // }, customIntegrations: { disabled: true, error: 'You can use all our official, built-in integrations on the Starter plan. If you upgrade to one of our higher tiers, you’ll also be able to create and edit custom integrations and API keys for advanced workflows.' From 8fb691b3eed0a74076a39f6372dea57fd0826691 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 11 May 2021 17:13:18 +0400 Subject: [PATCH 063/255] Added documentation for types of limits refs https://github.com/TryGhost/Team/issues/588 refs https://github.com/TryGhost/Team/issues/510 - There's a limited type of limits supported by the limit service and it's worth to have a conceptual description of how they work and how to use them --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 173fe271413..b8e4cca67e6 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,13 @@ if (limitService.isLimited('members')) { } ``` -In case the limit check is run without direct access to the database you can override `currentCountQuery` functions for each "max" type of limit. An example usecase would be a frontend client running in a browser. A browser client can check the limit data through HTTP request and then provide that data to the limit service. Example code to do exactly that: +### Types of limits +At the moment there are four different types of limits that limit service allows to define. These types are: +1. `flag` - is an "on/off" switch for certain feature. Example usecase: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. +2. `max` - checks if the maximum amount of the resource has been used up.Example usecase: "disable creating a staff user when maximum of 5 has been reached". To configure this limit add `max: NUMBER` to the configuration. The limits that support max checks are: `members`, `staff`, and `customIntegrations` +3. `maxPeriodic` - it's a variation of `max` type with a difference that the check is done over certain period of time. Example usecase: "disable sending emails when the sent emails count has acceded a limit for last billing period". To enable this limit define `maxPeriodic: NUMBER` in the limit configuration and provide a subscription configuration when initializing the limit service instance. The subscription object comes as a separate parameter and has to contain two properties: `startDate` and `interval`, where `startDate` is a date in ISO 8601 format and period is `'month'` (other values like `'year'` are not supported yet) +4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. + ```js const limitService = new LimitService(); From 36066cc5fdebd9b9a2c3f25097e3b989de5f625c Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 11 May 2021 17:14:58 +0400 Subject: [PATCH 064/255] Added documentation for names of limits refs https://github.com/TryGhost/Team/issues/510 - There's a limited type of limit "names" supported by the limit service, so worth specifying them upfront. Also some limits are univerally aplicable like "flag" or "allowlist" and some are restricted like "max" and "maxPeriodic" --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b8e4cca67e6..d0c4770717d 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,12 @@ At the moment there are four different types of limits that limit service allows 3. `maxPeriodic` - it's a variation of `max` type with a difference that the check is done over certain period of time. Example usecase: "disable sending emails when the sent emails count has acceded a limit for last billing period". To enable this limit define `maxPeriodic: NUMBER` in the limit configuration and provide a subscription configuration when initializing the limit service instance. The subscription object comes as a separate parameter and has to contain two properties: `startDate` and `interval`, where `startDate` is a date in ISO 8601 format and period is `'month'` (other values like `'year'` are not supported yet) 4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. +### Supported limits +There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`. + +All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff`, and`customIntegrations`) can have a `max` limit. Only `emails` currently supports the `maxPeriodic` type of limit. + + ```js const limitService = new LimitService(); From 7fff5ce34aff16074b9899d7aaa0e722c2ef7029 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 11 May 2021 17:15:22 +0400 Subject: [PATCH 065/255] Added header to the section no issue - Made it clear what this part of the doc is about --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d0c4770717d..de4975e7e93 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ There's a limited amount of limits that are supported by limit service. The are All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff`, and`customIntegrations`) can have a `max` limit. Only `emails` currently supports the `maxPeriodic` type of limit. +### Frontend usage +In case the limit check is run without direct access to the database you can override `currentCountQuery` functions for each "max" or "maxPeriodic" type of limit. An example usecase would be a frontend client running in a browser. A browser client can check the limit data through HTTP request and then provide that data to the limit service. Example code to do exactly that: ```js const limitService = new LimitService(); From fa06b378912b701e3efd805ed250d7c1d160036a Mon Sep 17 00:00:00 2001 From: Thibaut Patel Date: Wed, 12 May 2021 11:56:41 +0200 Subject: [PATCH 066/255] Updated the example for the customThemes configuration no issue --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index de4975e7e93..59a405d3c33 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,12 @@ const limits = { max: 1000, error: 'Your plan supports up to {{max}} members. Please upgrade to reenable publishing.' }, - // customThemes and customIntegrations are "flag" type of limits accepting disabled boolean configuration + // customThemes is an allowlist type of limit accepting the "allowlist" configuration customThemes: { - disabled: true, + allowlist: ['casper', 'dawn', 'lyra'], error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.' }, - // customThemes: { - // allowlist: ['casper', 'dawn', 'lyra'], - // error: "All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site." - // }, + // customIntegrations is a "flag" type of limits accepting disabled boolean configuration customIntegrations: { disabled: true, error: 'You can use all our official, built-in integrations on the Starter plan. If you upgrade to one of our higher tiers, you’ll also be able to create and edit custom integrations and API keys for advanced workflows.' From 2614071595c3bf9728c1fd0896916941ab22ca9e Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 12 May 2021 14:43:34 +0400 Subject: [PATCH 067/255] Exposed additional "name" variable in error templates refs https://github.com/TryGhost/Team/issues/587 - There was a need to be able to use the "name" of the limit inside of error templates like so: `{{name}}` (reference https://github.com/TryGhost/Team/issues/587#issuecomment-814281794) - This change allows to form custom error messages using following variable: `{{name}}` which is the same as the `name` property provided in the configuration for the limit --- lib/limit.js | 6 ++++-- test/limit-service.test.js | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index 7754f080cb4..b7f0377b984 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -66,7 +66,8 @@ class MaxLimit extends Limit { errorObj.message = _.template(this.error)( { max: Intl.NumberFormat().format(this.max), - count: Intl.NumberFormat().format(count) + count: Intl.NumberFormat().format(count), + name: this.name }); } catch (e) { errorObj.message = this.fallbackMessage; @@ -166,7 +167,8 @@ class MaxPeriodicLimit extends Limit { errorObj.message = _.template(this.error)( { max: Intl.NumberFormat().format(this.maxPeriodic), - count: Intl.NumberFormat().format(count) + count: Intl.NumberFormat().format(count), + name: this.name }); } catch (e) { errorObj.message = this.fallbackMessage; diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 1f22850f55a..c746f567311 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -36,20 +36,21 @@ describe('Limit Service', function () { error.errorDetails.total.should.eql(35000001); }); - it('Supports {{max}} and {{count}} variables', function () { + it('Supports {{max}}, {{count}}, and {{name}} variables', function () { let limit = new MaxLimit({ - name: 'test', + name: 'Test Resources', config: { max: 5, currentCountQuery: () => {}, - error: 'Your plan supports up to {{max}} staff users. You are currently at {{count}} staff users.Please upgrade to add more.' + error: '{{name}} limit reached. Your plan supports up to {{max}} staff users. You are currently at {{count}} staff users.Please upgrade to add more.' }, errors }); let error = limit.generateError(7); - error.message.should.eql('Your plan supports up to 5 staff users. You are currently at 7 staff users.Please upgrade to add more.'); + error.message.should.eql('Test Resources limit reached. Your plan supports up to 5 staff users. You are currently at 7 staff users.Please upgrade to add more.'); + error.errorDetails.name.should.eql('Test Resources'); error.errorDetails.limit.should.eql(5); error.errorDetails.total.should.eql(7); }); From 8fdb5b6e6bb93dd42b2ba1678cf34f776dcd817b Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 12 May 2021 16:42:36 +0400 Subject: [PATCH 068/255] Published new versions - @tryghost/job-manager@0.8.6 - @tryghost/limit-service@0.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 910c771d1cd..2d60b619868 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.5.0", + "version": "0.5.1", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 685b0a5a243a225d65a6a56a86ac58855dfeb5da Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 21 May 2021 13:24:55 +0400 Subject: [PATCH 069/255] Added a convenience method checking if any limits are acceded refs https://github.com/TryGhost/Team/issues/662 - There is a need to check if any of the current limits are over limit in Daisy. This method is the simplest possible implementation to check if any of them are over limit - Possible future iterations might include a list of names of the limits that have been acceded and their error messages - The `checkIfAnyOverLimit` method should be treated as a starter to work up the complexity as needed --- README.md | 5 +++ lib/limit-service.js | 15 ++++++++ test/limit-service.test.js | 70 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/README.md b/README.md index 59a405d3c33..9c4277df592 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,11 @@ if (limitService.isLimited('members')) { // useful in cases you need to check if specific instance would still be over the limit if the limit changed await limitService.errorIfIsOverLimit('members', {max: 10000}); } + +// check if any of the limits are acceding +if (limitService.checkIfAnyOverLimit()) { + console.log('One of the limits has acceded!'); +} ``` ### Types of limits diff --git a/lib/limit-service.js b/lib/limit-service.js index c2f8b242699..ed4b69e6559 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -102,6 +102,21 @@ class LimitService { await this.limits[limitName].errorIfWouldGoOverLimit(metadata); } + + /** + * Checks if any of the configured limits acced + * + * @returns {boolean} + */ + async checkIfAnyOverLimit() { + for (const limit in this.limits) { + if (await this.checkIsOverLimit(limit)) { + return true; + } + } + + return false; + } } module.exports = LimitService; diff --git a/test/limit-service.test.js b/test/limit-service.test.js index c746f567311..ec80558cab9 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -234,4 +234,74 @@ describe('Limit Service', function () { (await limitService.checkWouldGoOverLimit('members')).should.be.true(); }); }); + + describe('Check if any of configured limits are acceded', function () { + it('Confirms an acceded limit', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 5 + }, + members: { + max: 100, + currentCountQuery: () => 100 + }, + emails: { + maxPeriodic: 3, + currentCountQuery: () => 5 + }, + customIntegrations: { + disabled: true + } + }; + + const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, errors, subscription}); + + (await limitService.checkIfAnyOverLimit()).should.be.true(); + }); + + it('Does not confirm if no limits are acceded', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 1 + }, + members: { + max: 100, + currentCountQuery: () => 2 + }, + emails: { + maxPeriodic: 3, + currentCountQuery: () => 2 + }, + // TODO: allowlist type of limits doesn't have "checkIsOverLimit" implemented yet! + // customThemes: { + // allowlist: ['casper', 'dawn', 'lyra'] + // }, + // NOTE: the flag limit has flawed assumption of not being acceded previously + // this test might fail when the flaw is addressed + customIntegrations: { + disabled: true + } + }; + + const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, errors, subscription}); + + (await limitService.checkIfAnyOverLimit()).should.be.false(); + }); + }); }); From fe1757e2b0484e164de64c69d2feb861ec4c7d8a Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 21 May 2021 14:02:35 +0400 Subject: [PATCH 070/255] Fixed error "swallowing" no issue - I've discovered the "IncorrectUsageError" error was silently swallowed and the method returned a false positibe when an allowlist limit type was called with incorrect parameters - In cases like this it's best to surface the real error early otherwise the logic might produce unsafe results! --- lib/limit-service.js | 4 ++++ test/limit-service.test.js | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/limit-service.js b/lib/limit-service.js index ed4b69e6559..c0e8ed1030d 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -69,6 +69,8 @@ class LimitService { if (error instanceof this.errors.HostLimitError) { return true; } + + throw error; } } @@ -84,6 +86,8 @@ class LimitService { if (error instanceof this.errors.HostLimitError) { return true; } + + throw error; } } diff --git a/test/limit-service.test.js b/test/limit-service.test.js index ec80558cab9..bf3ead649c0 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -303,5 +303,25 @@ describe('Limit Service', function () { (await limitService.checkIfAnyOverLimit()).should.be.false(); }); + + it('Throws an error when an allowlist limit is checked', async function () { + const limitService = new LimitService(); + + let limits = { + // TODO: allowlist type of limits doesn't have "checkIsOverLimit" implemented yet! + customThemes: { + allowlist: ['casper', 'dawn', 'lyra'] + } + }; + + limitService.loadLimits({limits, errors}); + + try { + await limitService.checkIfAnyOverLimit(); + should.fail(limitService, 'Should have errored'); + } catch (err) { + err.message.should.eql(`Cannot read property 'value' of undefined`); + } + }); }); }); From cb4cb8007a66ed79f40296088de8d04f29126020 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 21 May 2021 14:09:27 +0400 Subject: [PATCH 071/255] Fixed indescribable error in allowlist limit https://github.com/TryGhost/Team/issues/663 - When there is no parameter passed at all it was a generic 'Cannot read property 'value' of undefined' message which wasn't helpful in recognizing what the actual problem was - Have added additional guarding logic to throw a descriptive error --- lib/limit.js | 4 ++-- test/limit-service.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index b7f0377b984..35645852ceb 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -289,7 +289,7 @@ class AllowlistLimit extends Limit { } async errorIfWouldGoOverLimit(metadata) { - if (!metadata.value) { + 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)) { @@ -298,7 +298,7 @@ class AllowlistLimit extends Limit { } async errorIfIsOverLimit(metadata) { - if (!metadata.value) { + 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)) { diff --git a/test/limit-service.test.js b/test/limit-service.test.js index bf3ead649c0..eff8fd6dbb5 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -320,7 +320,7 @@ describe('Limit Service', function () { await limitService.checkIfAnyOverLimit(); should.fail(limitService, 'Should have errored'); } catch (err) { - err.message.should.eql(`Cannot read property 'value' of undefined`); + err.message.should.eql(`Attempted to check an allowlist limit without a value`); } }); }); From d8386e0b48abe38bffbe63564f3026da96b4593f Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 21 May 2021 14:16:04 +0400 Subject: [PATCH 072/255] Published new versions - @tryghost/limit-service@0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d60b619868..e2cac996b6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.5.1", + "version": "0.6.0", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 318daa6644e9510ffb21b4ec5bc46426abdd6c4a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 24 May 2021 22:37:15 +0000 Subject: [PATCH 073/255] Update dependency sinon to v11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2cac996b6e..658ce8b2c13 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "mocha": "8.3.2", "should": "13.2.3", - "sinon": "10.0.0" + "sinon": "11.0.0" }, "dependencies": { "lodash": "^4.17.21", From 5538cb4cb0cae25d19e2c8263eebc0945b22baa8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 9 Jun 2021 09:08:29 +0000 Subject: [PATCH 074/255] Update dependency mocha to v9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 658ce8b2c13..61fa2eee0d5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "mocha": "8.3.2", + "mocha": "9.0.0", "should": "13.2.3", "sinon": "11.0.0" }, From 26ff925fb51630258f1871291ce5cfadca09a35b Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 9 Jun 2021 12:10:10 +0100 Subject: [PATCH 075/255] Published new versions - @tryghost/adapter-manager@0.2.13 - @tryghost/bootstrap-socket@0.2.9 - @tryghost/constants@0.1.8 - @tryghost/errors@0.2.12 - @tryghost/image-transform@1.0.12 - @tryghost/job-manager@0.8.7 - @tryghost/limit-service@0.6.1 - @tryghost/moleculer-service-from-class@0.2.16 - @tryghost/mw-session-from-token@0.1.21 - @tryghost/package-json@0.1.2 - @tryghost/pretty-cli@1.2.18 - @tryghost/promise@0.1.9 - @tryghost/release-utils@0.6.15 - @tryghost/security@0.2.9 - @tryghost/session-service@0.1.23 - @tryghost/tpl@0.1.0 - @tryghost/vhost-middleware@1.0.15 - @tryghost/zip@1.1.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 61fa2eee0d5..6bbf0fc6a2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.6.0", + "version": "0.6.1", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From fa8545b07403985c499adc3ef145a647f7393286 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 14 Jul 2021 11:26:06 +0100 Subject: [PATCH 076/255] Added c8 test coverage to all packages refs https://github.com/TryGhost/Team/issues/870 - using `c8` allows us to see test coverage for all packages in the repo - this commit adds `c8` as a dev dependency and prepends the `mocha` command with `c8` so it runs on all tests --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6bbf0fc6a2d..63b0ed6b81e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "exports": "./lib/limit-service.js", "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing mocha './test/**/*.test.js'", + "test": "NODE_ENV=testing c8 mocha './test/**/*.test.js'", "lint": "eslint . --ext .js --cache", "posttest": "yarn lint" }, @@ -20,6 +20,7 @@ "access": "public" }, "devDependencies": { + "c8": "7.7.3", "mocha": "9.0.0", "should": "13.2.3", "sinon": "11.0.0" From f8a17b75ca915f8c7da1249db14f0fbd2337d9d6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 28 Jul 2021 17:56:14 +0000 Subject: [PATCH 077/255] Update dependency c8 to v7.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63b0ed6b81e..fc6cb72fe2b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.7.3", + "c8": "7.8.0", "mocha": "9.0.0", "should": "13.2.3", "sinon": "11.0.0" From 758e1831607578b10218efc3f469b03b987a6522 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 10 Sep 2021 04:03:35 +0000 Subject: [PATCH 078/255] Update dependency c8 to v7.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc6cb72fe2b..ed417407507 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.8.0", + "c8": "7.9.0", "mocha": "9.0.0", "should": "13.2.3", "sinon": "11.0.0" From c9836ddfb6d5ea4e73a86e631a477325ca06768b Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 22 Sep 2021 11:32:02 +0200 Subject: [PATCH 079/255] Removed use of native JS Error objects refs https://linear.app/tryghost/issue/CORE-49/fix-errors-in-utils-repo-limit-service - The latest ESLint rules forbid use of native JS errors, updated the codebase before bumping the ESLint version --- lib/date-utils.js | 3 ++- lib/limit-service.js | 3 ++- package.json | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/date-utils.js b/lib/date-utils.js index 7e1943137bf..bae0847ffc1 100644 --- a/lib/date-utils.js +++ b/lib/date-utils.js @@ -1,4 +1,5 @@ const {DateTime} = require('luxon'); +const {IncorrectUsageError} = require('@tryghost/errors'); const SUPPORTED_INTERVALS = ['month']; /** @@ -21,7 +22,7 @@ const lastPeriodStart = (startDate, interval) => { return lastPeriodStartDate.toISO(); } - throw new Error('Invalid interval specified. Only "month" value is accepted.'); + throw new IncorrectUsageError('Invalid interval specified. Only "month" value is accepted.'); }; module.exports = { diff --git a/lib/limit-service.js b/lib/limit-service.js index c0e8ed1030d..3df5357af05 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -1,5 +1,6 @@ const {MaxLimit, MaxPeriodicLimit, FlagLimit, AllowlistLimit} = require('./limit'); const config = require('./config'); +const {IncorrectUsageError} = require('@tryghost/errors'); const _ = require('lodash'); class LimitService { @@ -19,7 +20,7 @@ class LimitService { */ loadLimits({limits = {}, subscription, helpLink, db, errors}) { if (!errors) { - throw new Error(`Config Missing: 'errors' is required.`); + throw new IncorrectUsageError(`Config Missing: 'errors' is required.`); } this.errors = errors; diff --git a/package.json b/package.json index ed417407507..fc9b6d82f62 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "sinon": "11.0.0" }, "dependencies": { + "@tryghost/errors": "^0.2.13", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 62ec08c648566a4a8db81c454e220cd9db5ef1e5 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 22 Sep 2021 11:51:37 +0200 Subject: [PATCH 080/255] Fixed error initialization syntax refs https://linear.app/tryghost/issue/CORE-9/remove-eslint-warnings - Used an incorrect string parameter constructor for ghost errors previously. The errors should be initialized with an object containing a "message" property --- lib/date-utils.js | 4 +++- lib/limit-service.js | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/date-utils.js b/lib/date-utils.js index bae0847ffc1..6798c4ec21f 100644 --- a/lib/date-utils.js +++ b/lib/date-utils.js @@ -22,7 +22,9 @@ const lastPeriodStart = (startDate, interval) => { return lastPeriodStartDate.toISO(); } - throw new IncorrectUsageError('Invalid interval specified. Only "month" value is accepted.'); + throw new IncorrectUsageError({ + message: 'Invalid interval specified. Only "month" value is accepted.' + }); }; module.exports = { diff --git a/lib/limit-service.js b/lib/limit-service.js index 3df5357af05..5996d685917 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -20,7 +20,9 @@ class LimitService { */ loadLimits({limits = {}, subscription, helpLink, db, errors}) { if (!errors) { - throw new IncorrectUsageError(`Config Missing: 'errors' is required.`); + throw new IncorrectUsageError({ + message: `Config Missing: 'errors' is required.` + }); } this.errors = errors; @@ -42,7 +44,9 @@ class LimitService { this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db, errors}); } else if (_.has(limitConfig, 'maxPeriodic')) { if (subscription === undefined) { - throw new errors.IncorrectUsageError({message: 'Attempted to setup a periodic max limit without a subscription'}); + throw new errors.IncorrectUsageError({ + message: 'Attempted to setup a periodic max limit without a subscription' + }); } const maxPeriodicLimitConfig = Object.assign({}, limitConfig, subscription); From 38ef7ae7f92852bad0c806a1b8db6cbc7bd4432d Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 22 Sep 2021 11:57:49 +0200 Subject: [PATCH 081/255] Moved error messages to "messages" hash refs https://linear.app/tryghost/issue/CORE-49/fix-errors-in-utils-repo-limit-service - As I've touched these files did a little refactor and changed where the error messages are stored to keep it up with our lates coding standard - having "messages" hash defined in the module storing all messages that have pottential for i18y in the future. --- lib/date-utils.js | 6 +++++- lib/limit-service.js | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/date-utils.js b/lib/date-utils.js index 6798c4ec21f..cc91f34019a 100644 --- a/lib/date-utils.js +++ b/lib/date-utils.js @@ -1,6 +1,10 @@ const {DateTime} = require('luxon'); const {IncorrectUsageError} = require('@tryghost/errors'); +const messages = { + invalidInterval: 'Invalid interval specified. Only "month" value is accepted.' +}; + const SUPPORTED_INTERVALS = ['month']; /** * Calculates the start of the last period (billing, cycle, etc.) based on the start date @@ -23,7 +27,7 @@ const lastPeriodStart = (startDate, interval) => { } throw new IncorrectUsageError({ - message: 'Invalid interval specified. Only "month" value is accepted.' + message: messages.invalidInterval }); }; diff --git a/lib/limit-service.js b/lib/limit-service.js index 5996d685917..4fb90f542dd 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -3,6 +3,11 @@ const config = require('./config'); const {IncorrectUsageError} = require('@tryghost/errors'); const _ = require('lodash'); +const messages = { + missingErrorsConfig: `Config Missing: 'errors' is required.`, + noSubscriptionParameter: 'Attempted to setup a periodic max limit without a subscription' +}; + class LimitService { constructor() { this.limits = {}; @@ -21,7 +26,7 @@ class LimitService { loadLimits({limits = {}, subscription, helpLink, db, errors}) { if (!errors) { throw new IncorrectUsageError({ - message: `Config Missing: 'errors' is required.` + message: messages.missingErrorsConfig }); } @@ -44,8 +49,8 @@ class LimitService { this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db, errors}); } else if (_.has(limitConfig, 'maxPeriodic')) { if (subscription === undefined) { - throw new errors.IncorrectUsageError({ - message: 'Attempted to setup a periodic max limit without a subscription' + throw new IncorrectUsageError({ + message: messages.noSubscriptionParameter }); } From 579d7f87cbeb5aa8e1c73c28baba94bf907ca3cf Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 22 Sep 2021 11:59:35 +0200 Subject: [PATCH 082/255] Published new versions - @tryghost/adapter-manager@0.2.15 - @tryghost/bootstrap-socket@0.2.10 - @tryghost/config-url-helpers@0.1.1 - @tryghost/constants@0.1.9 - @tryghost/errors@0.2.14 - @tryghost/image-transform@1.0.14 - @tryghost/job-manager@0.8.8 - @tryghost/limit-service@0.6.2 - @tryghost/moleculer-service-from-class@0.2.18 - @tryghost/mw-session-from-token@0.1.23 - @tryghost/package-json@1.0.3 - @tryghost/pretty-cli@1.2.19 - @tryghost/promise@0.1.10 - @tryghost/release-utils@0.6.16 - @tryghost/security@0.2.10 - @tryghost/session-service@0.1.25 - @tryghost/tpl@0.1.4 - @tryghost/vhost-middleware@1.0.16 - @tryghost/zip@1.1.15 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc9b6d82f62..01d7c0a9a94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.6.1", + "version": "0.6.2", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.0.0" }, "dependencies": { - "@tryghost/errors": "^0.2.13", + "@tryghost/errors": "^0.2.14", "lodash": "^4.17.21", "luxon": "^1.26.0" } From fd2d94b36ba717aed140f82df6e6f42495dfda26 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 22 Sep 2021 12:54:32 +0000 Subject: [PATCH 083/255] Update Test & linting packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 01d7c0a9a94..cec4b66a659 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,9 @@ }, "devDependencies": { "c8": "7.9.0", - "mocha": "9.0.0", + "mocha": "9.1.1", "should": "13.2.3", - "sinon": "11.0.0" + "sinon": "11.1.2" }, "dependencies": { "@tryghost/errors": "^0.2.14", From 128f5bb3da235bc3b583e94c375371b5b30f283e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 27 Sep 2021 01:04:25 +0000 Subject: [PATCH 084/255] Update dependency mocha to v9.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cec4b66a659..16dd2f9b7fe 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "7.9.0", - "mocha": "9.1.1", + "mocha": "9.1.2", "should": "13.2.3", "sinon": "11.1.2" }, From cd7bf31d57f9cf1a41c956fef85ff167a2bd137b Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 30 Sep 2021 19:23:46 +0200 Subject: [PATCH 085/255] Published new versions - @tryghost/adapter-manager@0.2.16 - @tryghost/bootstrap-socket@0.2.11 - @tryghost/config-url-helpers@0.1.2 - @tryghost/constants@0.1.10 - @tryghost/errors@0.2.15 - @tryghost/image-transform@1.0.15 - @tryghost/job-manager@0.8.9 - @tryghost/limit-service@0.6.3 - @tryghost/moleculer-service-from-class@0.2.19 - @tryghost/mw-session-from-token@0.1.24 - @tryghost/package-json@1.0.4 - @tryghost/pretty-cli@1.2.20 - @tryghost/promise@0.1.11 - @tryghost/release-utils@0.6.17 - @tryghost/security@0.2.11 - @tryghost/session-service@0.1.26 - @tryghost/settings-path-manager@0.1.0 - @tryghost/tpl@0.1.5 - @tryghost/vhost-middleware@1.0.17 - @tryghost/zip@1.1.16 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 16dd2f9b7fe..68d0eaa6315 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.6.2", + "version": "0.6.3", "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^0.2.14", + "@tryghost/errors": "^0.2.15", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 3faaa7eaf2bc50d298f3b8adf11d4e3b2301f1b7 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 1 Oct 2021 14:34:06 +0100 Subject: [PATCH 086/255] Updated repository links no issue - this repo changes from `master` to `main` a while back, but the repository links needed updating too --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68d0eaa6315..995442a1c0d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/limit-service", "version": "0.6.3", - "repository": "https://github.com/TryGhost/Utils/tree/master/packages/limit-service", + "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", "main": "./lib/limit-service.js", From 0d726f29636b64328c28bb3298362e49ffad64c8 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 1 Oct 2021 16:57:18 +0100 Subject: [PATCH 087/255] Published new versions - @tryghost/adapter-manager@0.2.17 - @tryghost/bootstrap-socket@0.2.12 - @tryghost/constants@0.1.11 - @tryghost/errors@0.2.16 - @tryghost/image-transform@1.0.16 - @tryghost/job-manager@0.8.10 - @tryghost/limit-service@0.6.4 - @tryghost/moleculer-service-from-class@0.2.20 - @tryghost/mw-session-from-token@0.1.25 - @tryghost/package-json@1.0.5 - @tryghost/pretty-cli@1.2.21 - @tryghost/promise@0.1.12 - @tryghost/release-utils@0.7.0 - @tryghost/security@0.2.12 - @tryghost/session-service@0.1.27 - @tryghost/vhost-middleware@1.0.18 - @tryghost/zip@1.1.17 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 995442a1c0d..9c53f40d1e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.6.3", + "version": "0.6.4", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^0.2.15", + "@tryghost/errors": "^0.2.16", "lodash": "^4.17.21", "luxon": "^1.26.0" } From ef3ae145b47d24254164998a6d773ba540c92fa2 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 6 Oct 2021 23:39:01 +0000 Subject: [PATCH 088/255] Update dependency c8 to v7.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c53f40d1e6..78585c29580 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.9.0", + "c8": "7.10.0", "mocha": "9.1.2", "should": "13.2.3", "sinon": "11.1.2" From f7b0a2cfef8f63fb903bc69bc9385d25808e7c93 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 15 Oct 2021 08:26:58 +0000 Subject: [PATCH 089/255] Update dependency mocha to v9.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78585c29580..af0411847e3 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "7.10.0", - "mocha": "9.1.2", + "mocha": "9.1.3", "should": "13.2.3", "sinon": "11.1.2" }, From 216a11faa7e7cbd796f11ed313c7c96bc8b65f1c Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 20 Oct 2021 11:48:19 +0200 Subject: [PATCH 090/255] Added codecov.io coverage uploader to CI refs linear.app/tryghost/issue/CORE-74/improve-the-test-situation - this commit adds the codecov GitHub Action into CI so we can upload coverage reports - the coverage files need to be in XML for them to work with codecov, so this commit also adds cobertura (XML) as a reporter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af0411847e3..1e1938017f6 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "exports": "./lib/limit-service.js", "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing c8 mocha './test/**/*.test.js'", + "test": "NODE_ENV=testing c8 --reporter text --reporter cobertura mocha './test/**/*.test.js'", "lint": "eslint . --ext .js --cache", "posttest": "yarn lint" }, From b4b06958e71b4bad19811585f8a81d759d7d0ce3 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 22 Oct 2021 16:01:20 +0400 Subject: [PATCH 091/255] Published new versions - @tryghost/adapter-manager@0.2.18 - @tryghost/bootstrap-socket@0.2.13 - @tryghost/config-url-helpers@0.1.3 - @tryghost/constants@0.1.12 - @tryghost/errors@0.2.17 - @tryghost/image-transform@1.0.17 - @tryghost/job-manager@0.8.11 - @tryghost/limit-service@0.6.5 - @tryghost/moleculer-service-from-class@0.2.21 - @tryghost/mw-session-from-token@0.1.26 - @tryghost/package-json@1.0.6 - @tryghost/pretty-cli@1.2.22 - @tryghost/promise@0.1.13 - @tryghost/release-utils@0.7.1 - @tryghost/security@0.2.13 - @tryghost/session-service@0.1.28 - @tryghost/settings-path-manager@0.1.2 - @tryghost/vhost-middleware@1.0.19 - @tryghost/zip@1.1.18 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1e1938017f6..d2c047161cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.6.4", + "version": "0.6.5", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^0.2.16", + "@tryghost/errors": "^0.2.17", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 90b6e6b6b3951fe62372dacee9b84f464a15f601 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 25 Oct 2021 18:18:04 +0400 Subject: [PATCH 092/255] Improved test coverage for limit-service module no issue - The aim is to achieve 100% unit test coverage for servies and small modules. This change covers few more bases brining limit-service's module coverage from 80% to 94%. --- test/limit-service.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/limit-service.test.js b/test/limit-service.test.js index eff8fd6dbb5..339f23ddacb 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -304,6 +304,22 @@ describe('Limit Service', function () { (await limitService.checkIfAnyOverLimit()).should.be.false(); }); + it('Returns nothing if limit is not configured', async function () { + const limitService = new LimitService(); + + const isOverLimitResult = await limitService.checkIsOverLimit('unlimited'); + should.equal(isOverLimitResult, undefined); + + const wouldGoOverLimitResult = await limitService.checkWouldGoOverLimit('unlimited'); + should.equal(wouldGoOverLimitResult, undefined); + + const errorIfIsOverLimitResult = await limitService.errorIfIsOverLimit('unlimited'); + should.equal(errorIfIsOverLimitResult, undefined); + + const errorIfWouldGoOverLimitResult = await limitService.errorIfWouldGoOverLimit('unlimited'); + should.equal(errorIfWouldGoOverLimitResult, undefined); + }); + it('Throws an error when an allowlist limit is checked', async function () { const limitService = new LimitService(); From a0cf4379cc2bde0bf5e59695bd411f4f0a1af9f6 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 10:48:27 +0400 Subject: [PATCH 093/255] Fixed returned value type no issue - The return type was incorrectly declared thworing error during type checking --- lib/limit-service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index 4fb90f542dd..1ec74fbb14b 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -118,9 +118,9 @@ class LimitService { } /** - * Checks if any of the configured limits acced + * Checks if any of the configured limits acceded * - * @returns {boolean} + * @returns {Promise} */ async checkIfAnyOverLimit() { for (const limit in this.limits) { From 1084f14280e080ff455fac36546256d78fff0051 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 10:51:25 +0400 Subject: [PATCH 094/255] Added JSDoc with types to the Limit base constructor no issue - Improved type checking a little --- lib/limit.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/limit.js b/lib/limit.js index 35645852ceb..1361b104359 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -5,6 +5,15 @@ 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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) + */ constructor({name, error, helpLink, db, errors}) { this.name = name; this.error = error; @@ -36,6 +45,7 @@ class MaxLimit extends 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.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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) From 994b65ee97e35172e30a4ffcc569a38713e65e6e Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 10:52:09 +0400 Subject: [PATCH 095/255] Fixed typos --- lib/limit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index 1361b104359..f16e035e8a9 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -95,7 +95,7 @@ class MaxLimit extends Limit { } /** - * Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery + * 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 @@ -109,7 +109,7 @@ class MaxLimit extends Limit { } /** - * Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery + * 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 @@ -198,7 +198,7 @@ class MaxPeriodicLimit extends Limit { } /** - * Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery + * 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 @@ -212,7 +212,7 @@ class MaxPeriodicLimit extends Limit { } /** - * Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery + * 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 From 5654f9ad3df0b0b86bb6793760d8694259f9bca8 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 11:11:59 +0400 Subject: [PATCH 096/255] Improved JSDocs in limit package no issue - There were a few errors and little inconsistencies that needed a cleanup --- lib/limit.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/limit.js b/lib/limit.js index f16e035e8a9..c9afaf5b45f 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -11,7 +11,7 @@ class Limit { * @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.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, error, helpLink, db, errors}) { @@ -99,6 +99,7 @@ class MaxLimit extends Limit { * * @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(this.db); @@ -130,6 +131,7 @@ class MaxPeriodicLimit extends Limit { * @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 @@ -202,6 +204,7 @@ class MaxPeriodicLimit extends Limit { * * @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(this.db); @@ -233,6 +236,7 @@ class FlagLimit extends Limit { * @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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) @@ -275,6 +279,16 @@ class FlagLimit extends Limit { } 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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) + */ constructor({name, config, helpLink, errors}) { super({name, error: config.error || '', helpLink, errors}); From 08bf0d318df2d36aee34ddeffae481f7580b0f6b Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 11:15:15 +0400 Subject: [PATCH 097/255] Fixed uses ov currentCountQuery no issue - The currentCountQuery method takes in no parameters! --- lib/limit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index c9afaf5b45f..f53ad7a5f3d 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -102,7 +102,7 @@ class MaxLimit extends Limit { * @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(this.db); + let currentCount = await this.currentCountQuery(); if ((currentCount + addedCount) > (max || this.max)) { throw this.generateError(currentCount); @@ -116,7 +116,7 @@ class MaxLimit extends Limit { * @param {Number} [options.max] - overrides configured default max value to perform checks against */ async errorIfIsOverLimit({max} = {}) { - let currentCount = await this.currentCountQuery(this.db); + let currentCount = await this.currentCountQuery(); if (currentCount > (max || this.max)) { throw this.generateError(currentCount); @@ -207,7 +207,7 @@ class MaxPeriodicLimit extends Limit { * @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(this.db); + let currentCount = await this.currentCountQuery(); if ((currentCount + addedCount) > (max || this.maxPeriodic)) { throw this.generateError(currentCount); @@ -221,7 +221,7 @@ class MaxPeriodicLimit extends Limit { * @param {Number} [options.max] - overrides configured default maxPeriodic value to perform checks against */ async errorIfIsOverLimit({max} = {}) { - let currentCount = await this.currentCountQuery(this.db); + let currentCount = await this.currentCountQuery(); if (currentCount > (max || this.maxPeriodic)) { throw this.generateError(currentCount); From fdf8a9556889e81bcda6f538eca7887457be58fd Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 11:23:42 +0400 Subject: [PATCH 098/255] Improved JSDocs in limit package no issue - There were a few errors and little inconsistencies that needed a cleanup --- lib/limit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index f53ad7a5f3d..b029ebfd507 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -46,8 +46,8 @@ class MaxLimit extends Limit { * @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.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 {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}) { @@ -136,7 +136,7 @@ class MaxPeriodicLimit extends 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.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}) { @@ -238,7 +238,7 @@ class FlagLimit extends Limit { * @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.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}) { From df333268b00e9f3536e49661a81cde236947fe09 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 15:42:10 +0400 Subject: [PATCH 099/255] Added ability to pass in "currentCount" for limited resource refs https://linear.app/tryghost/issue/CORE-121/create-a-video-storage-adapter - When checking limits for a nondb-resource type (like file size) there is no way to "currentCountQuery", so the value has to be passed in directly into the limit to evaluate against configured "max" limit --- lib/limit.js | 5 +++-- test/limit.test.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index b029ebfd507..61dd188fefc 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -114,9 +114,10 @@ class MaxLimit extends Limit { * * @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} = {}) { - let currentCount = await this.currentCountQuery(); + async errorIfIsOverLimit({max, currentCount} = {}) { + currentCount = currentCount || await this.currentCountQuery(); if (currentCount > (max || this.max)) { throw this.generateError(currentCount); diff --git a/test/limit.test.js b/test/limit.test.js index eaaa0057fa5..b8efaf79896 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -45,6 +45,23 @@ describe('Limit Service', function () { describe('Max Limit', function () { describe('Constructor', function () { + it('passes if within the limit and custom currentCount overriding currentCountQuery', async function () { + const config = { + max: 5, + error: 'You have gone over the limit', + currentCountQuery: function () { + throw new Error('Should not be called'); + } + }; + + try { + const limit = new MaxLimit({name: '', config, errors}); + await limit.errorIfIsOverLimit({currentCount: 4}); + } catch (error) { + should.fail('Should have not errored', error); + } + }); + it('throws if initialized without a max limit', function () { const config = {}; @@ -74,6 +91,34 @@ describe('Limit Service', function () { err.message.should.match(/max limit without a current count query/); } }); + + it('throws when would go over the limit and custom currentCount overriding currentCountQuery', async function () { + const _5MB = 5000000; + const config = { + max: _5MB, + error: 'You have exceeded the maximum file size {{ max }}', + currentCountQuery: function () { + throw new Error('Should not be called'); + } + }; + + try { + const limit = new MaxLimit({ + name: 'fileSize', + config, + errors + }); + const _10MB = 10000000; + + await limit.errorIfIsOverLimit({currentCount: _10MB}); + } catch (error) { + error.errorType.should.equal('HostLimitError'); + error.errorDetails.name.should.equal('fileSize'); + error.errorDetails.limit.should.equal(5000000); + error.errorDetails.total.should.equal(10000000); + error.message.should.equal('You have exceeded the maximum file size 5,000,000'); + } + }); }); describe('Is over limit', function () { From 6fc520351ec93d26ff77759f77f0d7c1a8af2ab9 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 15:46:36 +0400 Subject: [PATCH 100/255] Added "uploads" limit type refs https://linear.app/tryghost/issue/CORE-121/create-a-video-storage-adapter - The limit is here to accomodate file size checks - An example configuration is in the README --- README.md | 15 +++++++++++++-- lib/config.js | 8 +++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c4277df592..f625757de3a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ const LimitService = require('@tryghost/limit-service'); const limitService = new LimitService(); // setup limit configuration -// currently supported limit keys are: staff, members, customThemes, customIntegrations +// currently supported limit keys are: staff, members, customThemes, customIntegrations, uploads // all limit configs support custom "error" configuration that is a template string const limits = { // staff and member are "max" type of limits accepting "max" configuration @@ -59,6 +59,12 @@ const limits = { // maxPeriodic: 42, // error: 'Your plan supports up to {{max}} emails. Please upgrade to reenable sending emails.' // } + uploads: { + // max key is in bytes + max: 5000000, + // formatting of the {{ max }} vairable is in MB, e.g: 5MB + error: 'Your plan supports uploads of max size up to {{max}}. Please upgrade to reenable uploading.' + } }; // This information is needed for the limit service to work with "max periodic" limits @@ -116,6 +122,11 @@ if (limitService.isLimited('members')) { await limitService.errorIfIsOverLimit('members', {max: 10000}); } +if (limitService.isLimited('uploads')) { + // for the uploads limit we HAVE TO pass in the "currentCount" parameter and use bytes as a base unit + await limitService.errorIfIsOverLimit('uploads', {currentCount: frame.file.size}); +} + // check if any of the limits are acceding if (limitService.checkIfAnyOverLimit()) { console.log('One of the limits has acceded!'); @@ -130,7 +141,7 @@ At the moment there are four different types of limits that limit service allows 4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. ### Supported limits -There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`. +There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`, `uploads`. All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff`, and`customIntegrations`) can have a `max` limit. Only `emails` currently supports the `maxPeriodic` type of limit. diff --git a/lib/config.js b/lib/config.js index 3e4aff0afd5..e624cab815a 100644 --- a/lib/config.js +++ b/lib/config.js @@ -45,5 +45,11 @@ module.exports = { return result.count; } }, - customThemes: {} + customThemes: {}, + uploads: { + // NOTE: this function should not ever be used as for uploads we compare the size + // of the uploaded file with the configured limit. Noop is here to keep the + // MaxLimit constructor happy + currentCountQuery: () => {} + } }; From 6a33555f5b6ed721da34629b830186ac8cfc2b4e Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 15:49:26 +0400 Subject: [PATCH 101/255] Added custom formatter functionality to MaxLimit refs https://linear.app/tryghost/issue/CORE-121/create-a-video-storage-adapter - Some variables (like file size) would be hard to comprehend with the default formatting. Instead allowed MaxLimit to be configured with a custom formatter --- lib/limit.js | 12 ++++++++++-- test/limit.test.js | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/limit.js b/lib/limit.js index 61dd188fefc..0b1002bfbb8 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -45,6 +45,7 @@ class MaxLimit extends 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 @@ -63,20 +64,27 @@ class MaxLimit extends Limit { 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: Intl.NumberFormat().format(this.max), - count: Intl.NumberFormat().format(count), + max: formatter(this.max), + count: formatter(count), name: this.name }); } catch (e) { diff --git a/test/limit.test.js b/test/limit.test.js index b8efaf79896..644a4a81ea8 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -96,6 +96,7 @@ describe('Limit Service', function () { const _5MB = 5000000; const config = { max: _5MB, + formatter: count => `${count / 1000000}MB`, error: 'You have exceeded the maximum file size {{ max }}', currentCountQuery: function () { throw new Error('Should not be called'); @@ -116,7 +117,7 @@ describe('Limit Service', function () { error.errorDetails.name.should.equal('fileSize'); error.errorDetails.limit.should.equal(5000000); error.errorDetails.total.should.equal(10000000); - error.message.should.equal('You have exceeded the maximum file size 5,000,000'); + error.message.should.equal('You have exceeded the maximum file size 5MB'); } }); }); From 5906b1af53f17b5b4d8176692e257bf0e226988c Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 15:49:59 +0400 Subject: [PATCH 102/255] Added custom formatter to uploads limit refs https://linear.app/tryghost/issue/CORE-121/create-a-video-storage-adapter - Provides readable bytes -> megabytes conversion for filesize limit --- lib/config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index e624cab815a..a8fb16e8d66 100644 --- a/lib/config.js +++ b/lib/config.js @@ -50,6 +50,9 @@ module.exports = { // NOTE: this function should not ever be used as for uploads we compare the size // of the uploaded file with the configured limit. Noop is here to keep the // MaxLimit constructor happy - currentCountQuery: () => {} + currentCountQuery: () => {}, + // NOTE: the uploads limit is based on file sizes provided in Bytes + // a custom formatter is here for more user-friendly formatting when forming an error + formatter: count => `${count / 1000000}MB` } }; From 5da66ca477903621cd5db59906af4005dc8de94b Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 15:50:43 +0400 Subject: [PATCH 103/255] Added missing "should" imports no issue --- test/limit-service.test.js | 2 +- test/limit.test.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 339f23ddacb..6340f443373 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -1,7 +1,7 @@ // Switch these lines once there are useful utils // const testUtils = require('./utils'); require('./utils'); - +const should = require('should'); const LimitService = require('../lib/limit-service'); const {MaxLimit, MaxPeriodicLimit, FlagLimit} = require('../lib/limit'); diff --git a/test/limit.test.js b/test/limit.test.js index 644a4a81ea8..bd9e0209694 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -1,6 +1,7 @@ // Switch these lines once there are useful utils // const testUtils = require('./utils'); require('./utils'); +const should = require('should'); const errors = require('./fixtures/errors'); const {MaxLimit, AllowlistLimit, FlagLimit, MaxPeriodicLimit} = require('../lib/limit'); From 11e4f1cc624114d1291badf2cdeb96e1d8385eb1 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 15:51:15 +0400 Subject: [PATCH 104/255] Updated JSDocs for limit-service module no issue --- lib/limit-service.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/limit-service.js b/lib/limit-service.js index 1ec74fbb14b..c504b4b5720 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -101,6 +101,12 @@ class LimitService { } } + /** + * + * @param {String} limitName - name of the configured limit + * @param {Object} metadata - limit parameters + * @returns + */ async errorIfIsOverLimit(limitName, metadata = {}) { if (!this.isLimited(limitName)) { return; @@ -109,6 +115,12 @@ class LimitService { await this.limits[limitName].errorIfIsOverLimit(metadata); } + /** + * + * @param {String} limitName - name of the configured limit + * @param {Object} metadata - limit parameters + * @returns + */ async errorIfWouldGoOverLimit(limitName, metadata = {}) { if (!this.isLimited(limitName)) { return; From 827d6bef26bc39821d157c8191ae1502441bb8c1 Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 26 Oct 2021 15:52:50 +0400 Subject: [PATCH 105/255] Published new versions - @tryghost/limit-service@1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d2c047161cc..6a14fa25afe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "0.6.5", + "version": "1.0.0", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 31f58af26c8e378761d2e88a3bd2dd584c233681 Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 30 Nov 2021 11:14:50 +0000 Subject: [PATCH 106/255] Combine @tryghost/ignition-errors with @tryghost/errors refs: https://github.com/TryGhost/Toolbox/issues/147 --- lib/limit-service.js | 2 +- lib/limit.js | 10 +++++----- test/fixtures/errors.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index c504b4b5720..7ee9b95364f 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -21,7 +21,7 @@ class LimitService { * @param {Object} [options.subscription] - hash containing subscription configuration with interval and startDate properties * @param {String} options.helpLink - URL pointing to help resources for when limit is reached * @param {Object} options.db - knex db connection instance or other data source for the limit checks - * @param {Object} options.errors - instance of errors compatible with Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) + * @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors) */ loadLimits({limits = {}, subscription, helpLink, db, errors}) { if (!errors) { diff --git a/lib/limit.js b/lib/limit.js index 0b1002bfbb8..db2ae463e95 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -12,7 +12,7 @@ class 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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) + * @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors) */ constructor({name, error, helpLink, db, errors}) { this.name = name; @@ -49,7 +49,7 @@ class MaxLimit extends 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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) + * @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}); @@ -146,7 +146,7 @@ class MaxPeriodicLimit extends Limit { * @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) + * @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}); @@ -248,7 +248,7 @@ class FlagLimit extends 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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) + * @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}); @@ -296,7 +296,7 @@ class AllowlistLimit extends Limit { * @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 Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) + * @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}); diff --git a/test/fixtures/errors.js b/test/fixtures/errors.js index 5968fa03539..7512ccaa5e1 100644 --- a/test/fixtures/errors.js +++ b/test/fixtures/errors.js @@ -18,7 +18,7 @@ class HostLimitError extends Error { } } -// NOTE: this module is here to serve as a dummy fixture for Ghost-Ignition's errors (https://github.com/TryGhost/Ignition#errors) +// NOTE: this module is here to serve as a dummy fixture for GhostError errors (@tryghost/errors) module.exports = { IncorrectUsageError, HostLimitError From 0ef1cc707eeee33cf09532c40e285e5c34bd5d5d Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 30 Nov 2021 11:31:51 +0000 Subject: [PATCH 107/255] Published new versions - @tryghost/adapter-manager@0.2.19 - @tryghost/errors@1.0.0 - @tryghost/image-transform@1.0.19 - @tryghost/job-manager@0.8.14 - @tryghost/limit-service@1.0.1 - @tryghost/minifier@0.1.2 - @tryghost/package-json@1.0.7 - @tryghost/release-utils@0.7.2 - @tryghost/session-service@0.1.29 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6a14fa25afe..bed6c546871 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.0", + "version": "1.0.1", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^0.2.17", + "@tryghost/errors": "^1.0.0", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 39713b72f665453fc7013679a341e3ad4c6aed4e Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 30 Nov 2021 13:59:24 +0000 Subject: [PATCH 108/255] Published new versions - @tryghost/adapter-manager@0.2.20 - @tryghost/errors@1.0.1 - @tryghost/image-transform@1.0.20 - @tryghost/job-manager@0.8.15 - @tryghost/limit-service@1.0.2 - @tryghost/minifier@0.1.3 - @tryghost/package-json@1.0.8 - @tryghost/release-utils@0.7.3 - @tryghost/session-service@0.1.30 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bed6c546871..90dc3c60cea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.1", + "version": "1.0.2", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.0.0", + "@tryghost/errors": "^1.0.1", "lodash": "^4.17.21", "luxon": "^1.26.0" } From c02361a0ba439bb230c7f0d8a4ec7adae74ef50b Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 30 Nov 2021 14:41:30 +0000 Subject: [PATCH 109/255] Published new versions - @tryghost/adapter-manager@0.2.21 - @tryghost/errors@1.0.2 - @tryghost/image-transform@1.0.21 - @tryghost/limit-service@1.0.3 - @tryghost/minifier@0.1.4 - @tryghost/package-json@1.0.9 - @tryghost/release-utils@0.7.4 - @tryghost/session-service@0.1.31 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 90dc3c60cea..581a5d2fabb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.2", + "version": "1.0.3", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.0.1", + "@tryghost/errors": "^1.0.2", "lodash": "^4.17.21", "luxon": "^1.26.0" } From e19e568cce07fd861e11510065aa91cad334ad3d Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 30 Nov 2021 16:37:15 +0000 Subject: [PATCH 110/255] Published new versions - @tryghost/adapter-manager@0.2.22 - @tryghost/errors@1.0.3 - @tryghost/image-transform@1.0.22 - @tryghost/limit-service@1.0.4 - @tryghost/minifier@0.1.5 - @tryghost/package-json@1.0.10 - @tryghost/release-utils@0.7.5 - @tryghost/session-service@0.1.32 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 581a5d2fabb..44bb41c340e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.3", + "version": "1.0.4", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.0.2", + "@tryghost/errors": "^1.0.3", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 1a3e387d167a8936092991e12a44b18ab638acf2 Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 30 Nov 2021 17:12:21 +0000 Subject: [PATCH 111/255] Published new versions - @tryghost/adapter-manager@0.2.23 - @tryghost/errors@1.0.4 - @tryghost/image-transform@1.0.23 - @tryghost/limit-service@1.0.5 - @tryghost/minifier@0.1.6 - @tryghost/package-json@1.0.11 - @tryghost/release-utils@0.7.6 - @tryghost/session-service@0.1.33 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 44bb41c340e..805ae01e672 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.4", + "version": "1.0.5", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.0.3", + "@tryghost/errors": "^1.0.4", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 0452be79a8beb89d12bd8c2303cfba26ff9f2dbe Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Wed, 1 Dec 2021 17:29:28 +0000 Subject: [PATCH 112/255] Published new versions - @tryghost/adapter-manager@0.2.24 - @tryghost/errors@1.1.0 - @tryghost/image-transform@1.0.24 - @tryghost/limit-service@1.0.6 - @tryghost/minifier@0.1.7 - @tryghost/package-json@1.0.12 - @tryghost/release-utils@0.7.7 - @tryghost/session-service@0.1.34 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 805ae01e672..d02b7fa1401 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.5", + "version": "1.0.6", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.0.4", + "@tryghost/errors": "^1.1.0", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 03f6dbd4cc81cd7e7e7e6dca23f984e1408a7372 Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Mon, 6 Dec 2021 16:53:49 +0000 Subject: [PATCH 113/255] Published new versions - @tryghost/adapter-manager@0.2.25 - @tryghost/errors@1.1.1 - @tryghost/image-transform@1.0.25 - @tryghost/limit-service@1.0.7 - @tryghost/minifier@0.1.8 - @tryghost/package-json@1.0.13 - @tryghost/release-utils@0.7.8 - @tryghost/session-service@0.1.35 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d02b7fa1401..047c701e782 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.6", + "version": "1.0.7", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.1.0", + "@tryghost/errors": "^1.1.1", "lodash": "^4.17.21", "luxon": "^1.26.0" } From c30cbd295d129a9617d1a4d72835b17f08ead882 Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 14 Dec 2021 14:24:31 +0000 Subject: [PATCH 114/255] Published new versions - @tryghost/adapter-manager@0.2.26 - @tryghost/errors@1.2.0 - @tryghost/image-transform@1.0.26 - @tryghost/limit-service@1.0.8 - @tryghost/minifier@0.1.9 - @tryghost/mw-error-handler@0.1.1 - @tryghost/package-json@1.0.14 - @tryghost/release-utils@0.7.9 - @tryghost/session-service@0.1.36 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 047c701e782..1b93775a63d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.7", + "version": "1.0.8", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.1.1", + "@tryghost/errors": "^1.2.0", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 0ca654f2232afeed755ac1857e9ce0c2a939b080 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 30 Dec 2021 16:28:02 +0000 Subject: [PATCH 115/255] Update dependency c8 to v7.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b93775a63d..3a3a6abef70 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.10.0", + "c8": "7.11.0", "mocha": "9.1.3", "should": "13.2.3", "sinon": "11.1.2" From 82581e4a39ae538904745e502ad41081c6027d1f Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Thu, 6 Jan 2022 09:52:35 +0000 Subject: [PATCH 116/255] 2022 --- LICENSE | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 366ae5f6246..19bcb01bef9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2013-2021 Ghost Foundation +Copyright (c) 2013-2022 Ghost Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f625757de3a..4f1cf5b8d02 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ const limits = { // The startDate has to be in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601) const subscription = { interval: 'month', - startDate: '2021-09-18T19:00:52Z' + startDate: '2022-09-18T19:00:52Z' }; // initialize the URL linking to help documentation etc. @@ -200,4 +200,4 @@ Follow the instructions for the top-level repo. # Copyright & License -Copyright (c) 2013-2021 Ghost Foundation - Released under the [MIT license](LICENSE). +Copyright (c) 2013-2022 Ghost Foundation - Released under the [MIT license](LICENSE). From 8ef50fd9fd4d571291e38108cf6bdc2408c6f00d Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 18 Jan 2022 09:08:09 +0000 Subject: [PATCH 117/255] Published new versions - @tryghost/adapter-manager@0.2.27 - @tryghost/bootstrap-socket@0.2.16 - @tryghost/config-url-helpers@0.1.4 - @tryghost/constants@1.0.1 - @tryghost/database-info@0.1.0 - @tryghost/errors@1.2.1 - @tryghost/image-transform@1.0.27 - @tryghost/job-manager@0.8.18 - @tryghost/limit-service@1.0.9 - @tryghost/minifier@0.1.10 - @tryghost/moleculer-service-from-class@0.2.22 - @tryghost/mw-error-handler@0.1.2 - @tryghost/mw-session-from-token@0.1.27 - @tryghost/mw-update-user-last-seen@0.1.2 - @tryghost/package-json@1.0.15 - @tryghost/pretty-cli@1.2.23 - @tryghost/promise@0.1.14 - @tryghost/release-utils@0.7.10 - @tryghost/security@0.2.14 - @tryghost/session-service@0.1.37 - @tryghost/settings-path-manager@0.1.3 - @tryghost/vhost-middleware@1.0.20 - @tryghost/zip@1.1.19 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3a3a6abef70..e3c7f9829f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.8", + "version": "1.0.9", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", @@ -26,7 +26,7 @@ "sinon": "11.1.2" }, "dependencies": { - "@tryghost/errors": "^1.2.0", + "@tryghost/errors": "^1.2.1", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 11f8cd7126f1e6dac2870556b1740c079158f1f3 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 21 Feb 2022 12:50:26 +0100 Subject: [PATCH 118/255] Added `--all` flag to c8 commands refs https://github.com/TryGhost/Toolbox/issues/203 - without `--all`, c8 will ignore files that aren't covered in tests, so they won't pull the test coverage down - this means we have artificially high coverage scores - this commit adds `--all` where previously missing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3c7f9829f8..c38c9e38193 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "exports": "./lib/limit-service.js", "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing c8 --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", "lint": "eslint . --ext .js --cache", "posttest": "yarn lint" }, From 16a36eb390dbe4c83e9b7f369f85fd88cc039990 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 1 Mar 2022 08:40:52 +0100 Subject: [PATCH 119/255] Published new versions - @tryghost/adapter-manager@0.2.28 - @tryghost/bootstrap-socket@0.2.17 - @tryghost/config-url-helpers@0.1.5 - @tryghost/constants@1.0.2 - @tryghost/database-info@0.2.0 - @tryghost/image-transform@1.0.28 - @tryghost/job-manager@0.8.20 - @tryghost/limit-service@1.0.10 - @tryghost/minifier@0.1.11 - @tryghost/moleculer-service-from-class@0.2.23 - @tryghost/mw-error-handler@0.1.3 - @tryghost/mw-session-from-token@0.1.28 - @tryghost/mw-update-user-last-seen@0.1.3 - @tryghost/package-json@1.0.16 - @tryghost/pretty-cli@1.2.24 - @tryghost/promise@0.1.15 - @tryghost/release-utils@0.7.12 - @tryghost/security@0.2.15 - @tryghost/session-service@0.1.38 - @tryghost/settings-path-manager@0.1.4 - @tryghost/vhost-middleware@1.0.22 - @tryghost/zip@1.1.20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c38c9e38193..7917928695b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.9", + "version": "1.0.10", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From e2bdc9a1d973cfa268038a332b2454cb40ea7a08 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 20 Apr 2022 18:23:57 +0000 Subject: [PATCH 120/255] Update dependency c8 to v7.11.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7917928695b..2c2818d45d0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.11.0", + "c8": "7.11.2", "mocha": "9.1.3", "should": "13.2.3", "sinon": "11.1.2" From e1653ad2d03b2b962493415b241b1bf614c9de71 Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 21 Apr 2022 20:58:17 +0800 Subject: [PATCH 121/255] Published new versions - @tryghost/adapter-manager@0.2.29 - @tryghost/api-version-compatibility-service@0.1.0 - @tryghost/bootstrap-socket@0.2.18 - @tryghost/config-url-helpers@0.1.6 - @tryghost/constants@1.0.3 - @tryghost/database-info@0.3.2 - @tryghost/image-transform@1.0.30 - @tryghost/job-manager@0.8.22 - @tryghost/limit-service@1.0.11 - @tryghost/minifier@0.1.13 - @tryghost/moleculer-service-from-class@0.2.24 - @tryghost/mw-api-version-mismatch@0.1.0 - @tryghost/mw-error-handler@0.2.1 - @tryghost/mw-session-from-token@0.1.29 - @tryghost/mw-update-user-last-seen@0.1.4 - @tryghost/package-json@1.0.19 - @tryghost/pretty-cli@1.2.25 - @tryghost/promise@0.1.16 - @tryghost/release-utils@0.7.13 - @tryghost/security@0.2.16 - @tryghost/session-service@0.1.39 - @tryghost/settings-path-manager@0.1.5 - @tryghost/vhost-middleware@1.0.23 - @tryghost/zip@1.1.23 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c2818d45d0..84513065812 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.10", + "version": "1.0.11", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 3686df01e2dc21274346bb9d2213173a37b50ad2 Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Wed, 27 Apr 2022 11:21:15 +0100 Subject: [PATCH 122/255] Added newsletter flag to limits service refs https://github.com/TryGhost/Team/issues/1549 Adding the name of the flag to config.js is a requirment for using the flag --- lib/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/config.js b/lib/config.js index a8fb16e8d66..1b3080209ec 100644 --- a/lib/config.js +++ b/lib/config.js @@ -9,6 +9,7 @@ module.exports = { return result.count; } }, + newsletters: {}, emails: { currentCountQuery: async (db, startDate) => { let result = await db.knex('emails') From 71def6d808723637dbb0d06536563c4945935edb Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Wed, 27 Apr 2022 11:24:20 +0100 Subject: [PATCH 123/255] Published new versions - @tryghost/limit-service@1.1.0 - @tryghost/mw-error-handler@0.2.2 - @tryghost/mw-session-from-token@0.1.30 - @tryghost/session-service@0.1.40 - @tryghost/vhost-middleware@1.0.24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84513065812..64e6b26d47b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.0.11", + "version": "1.1.0", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 14fd3e24dd05a9b046e3cbfbc2239e052a389486 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 2 May 2022 13:54:55 +0000 Subject: [PATCH 124/255] Update Test & linting packages --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64e6b26d47b..ca94bb27494 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "7.11.2", - "mocha": "9.1.3", + "mocha": "9.2.2", "should": "13.2.3", "sinon": "11.1.2" }, From ea645bc0e586eeafb4fdbf031a42cefd504b0e4b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 2 May 2022 13:59:30 +0000 Subject: [PATCH 125/255] Update Test & linting packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ca94bb27494..89497ac74d5 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,9 @@ }, "devDependencies": { "c8": "7.11.2", - "mocha": "9.2.2", + "mocha": "10.0.0", "should": "13.2.3", - "sinon": "11.1.2" + "sinon": "13.0.2" }, "dependencies": { "@tryghost/errors": "^1.2.1", From 49391a0080d9fb513d931c6cc53ea39830e19d80 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 4 May 2022 17:21:51 +0800 Subject: [PATCH 126/255] Published new versions - @tryghost/adapter-manager@0.2.30 - @tryghost/api-version-compatibility-service@0.1.2 - @tryghost/bootstrap-socket@0.2.19 - @tryghost/config-url-helpers@0.1.7 - @tryghost/constants@1.0.4 - @tryghost/database-info@0.3.4 - @tryghost/email-content-generator@0.1.0 - @tryghost/image-transform@1.0.31 - @tryghost/job-manager@0.8.23 - @tryghost/limit-service@1.1.1 - @tryghost/minifier@0.1.14 - @tryghost/moleculer-service-from-class@0.2.25 - @tryghost/mw-api-version-mismatch@0.1.2 - @tryghost/mw-error-handler@0.2.3 - @tryghost/mw-session-from-token@0.1.31 - @tryghost/mw-update-user-last-seen@0.1.5 - @tryghost/package-json@1.0.20 - @tryghost/pretty-cli@1.2.26 - @tryghost/promise@0.1.17 - @tryghost/release-utils@0.7.14 - @tryghost/security@0.2.17 - @tryghost/session-service@0.1.41 - @tryghost/settings-path-manager@0.1.6 - @tryghost/update-check-service@0.3.3 - @tryghost/version-notifications-data-service@0.1.1 - @tryghost/vhost-middleware@1.0.25 - @tryghost/zip@1.1.24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89497ac74d5..111a388a16e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.1.0", + "version": "1.1.1", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 032de16c581c90307f4f13d74ec2db6ad07d5513 Mon Sep 17 00:00:00 2001 From: Aileen Nowak Date: Wed, 4 May 2022 11:52:57 -0400 Subject: [PATCH 127/255] Added currentCountQuery for newsletters refs https://github.com/TryGhost/Team/issues/1583 --- lib/config.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index 1b3080209ec..b5cfd27e779 100644 --- a/lib/config.js +++ b/lib/config.js @@ -9,7 +9,16 @@ module.exports = { return result.count; } }, - newsletters: {}, + newsletters: { + currentCountQuery: async (db) => { + let result = await db.knex('newsletters') + .count('id', {as: 'count'}) + .where('status', '=', 'active') + .first(); + + return result.count; + } + }, emails: { currentCountQuery: async (db, startDate) => { let result = await db.knex('emails') From c89bf5e11e07804bc7658489d24dae1d828a4aa0 Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 5 May 2022 17:57:44 +0800 Subject: [PATCH 128/255] Published new versions - @tryghost/api-version-compatibility-service@0.2.0 - @tryghost/email-content-generator@0.1.1 - @tryghost/limit-service@1.1.2 - @tryghost/mw-api-version-mismatch@0.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 111a388a16e..009dce0bdcb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.1.1", + "version": "1.1.2", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From dc161ccfe3ef684dd357796f68638a98aefef879 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 9 May 2022 00:14:50 +0000 Subject: [PATCH 129/255] Update dependency sinon to v14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 009dce0bdcb..2ecd99908c4 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.11.2", "mocha": "10.0.0", "should": "13.2.3", - "sinon": "13.0.2" + "sinon": "14.0.0" }, "dependencies": { "@tryghost/errors": "^1.2.1", From 89d5ded2b4e69510d8634f267dcffccd75c1c988 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 9 May 2022 18:25:48 +0800 Subject: [PATCH 130/255] Published new versions - @tryghost/adapter-manager@0.2.31 - @tryghost/api-version-compatibility-service@0.3.0 - @tryghost/bootstrap-socket@0.2.20 - @tryghost/config-url-helpers@0.1.8 - @tryghost/constants@1.0.5 - @tryghost/database-info@0.3.5 - @tryghost/email-content-generator@0.1.2 - @tryghost/image-transform@1.0.32 - @tryghost/job-manager@0.8.24 - @tryghost/limit-service@1.1.3 - @tryghost/minifier@0.1.15 - @tryghost/moleculer-service-from-class@0.2.26 - @tryghost/mw-api-version-mismatch@0.1.4 - @tryghost/mw-error-handler@1.0.1 - @tryghost/mw-session-from-token@0.1.32 - @tryghost/mw-update-user-last-seen@0.1.6 - @tryghost/package-json@1.0.21 - @tryghost/pretty-cli@1.2.27 - @tryghost/promise@0.1.18 - @tryghost/release-utils@0.7.15 - @tryghost/security@0.3.1 - @tryghost/session-service@0.1.42 - @tryghost/settings-path-manager@0.1.7 - @tryghost/update-check-service@0.3.4 - @tryghost/version-notifications-data-service@0.1.2 - @tryghost/zip@1.1.25 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ecd99908c4..4358d053644 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.1.2", + "version": "1.1.3", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 0b08b918e1ccc28e03ac130fec2201bce121b369 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Thu, 12 May 2022 13:40:41 +0200 Subject: [PATCH 131/255] Added transaction support to limit-service (#190) refs https://github.com/TryGhost/Ghost/pull/14780 refs https://github.com/TryGhost/Team/issues/1583 - We need transaction support in the limit-service so that we can run the count queries in the same transaction - This is required to avoid deadlocks when we check the limits when a transaction is in progress on the same tables - This issue specifically is required for newsletters, where we start a transaction when creating a newsletter. - Bumped `eslint-plugin-ghost` so we have newer ECMA features available - Updated README - Renamed `metadata` to `options` in `limit-service` --- README.md | 37 ++++++-- lib/config.js | 22 ++--- lib/limit-service.js | 44 +++++++--- lib/limit.js | 43 +++++++--- test/limit-service.test.js | 171 +++++++++++++++++++++++++++++++++++++ test/limit.test.js | 151 ++++++++++++++++++++++++++++++++ 6 files changed, 422 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 4f1cf5b8d02..85caf91ae93 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ or Below is a sample code to wire up limit service and perform few common limit checks: ```js +const knex = require('knex'); const errors = require('@tryghost/errors'); const LimitService = require('@tryghost/limit-service'); @@ -80,15 +81,17 @@ const subscription = { const helpLink = 'https://ghost.org/help/'; // initialize knex db connection for the limit service to use when running query checks -const db = knex({ - client: 'mysql', - connection: { - user: 'root', - password: 'toor', - host: 'localhost', - database: 'ghost', - } -}); +const db = { + knex: knex({ + client: 'mysql', + connection: { + user: 'root', + password: 'toor', + host: 'localhost', + database: 'ghost', + } + }); +}; // finish initializing the limits service limitService.loadLimits({limits, subscription, db, helpLink, errors}); @@ -133,6 +136,22 @@ if (limitService.checkIfAnyOverLimit()) { } ``` +### Transactions + +Some limit types (`max` or `maxPeriodic`) need to fetch the current count from the database. Sometimes you need those checks to also run in a transaction. To fix that, you can pass the `transacting` option to all the available checks. + +```js +db.transaction((transacting) => { + const options = {transacting}; + + await limitService.errorIfWouldGoOverLimit('newsletters', options); + await limitService.errorIfIsOverLimit('newsletters', options); + const a = await limitService.checkIsOverLimit('newsletters', options); + const b = await limitService.checkWouldGoOverLimit('newsletters', options); + const c = await limitService.checkIfAnyOverLimit(options); +}); +``` + ### Types of limits At the moment there are four different types of limits that limit service allows to define. These types are: 1. `flag` - is an "on/off" switch for certain feature. Example usecase: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. diff --git a/lib/config.js b/lib/config.js index b5cfd27e779..4166a60298c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -4,14 +4,14 @@ // 2. MaxLimit should contain a `currentCountQuery` function which would count the resources under limit module.exports = { members: { - currentCountQuery: async (db) => { - let result = await db.knex('members').count('id', {as: 'count'}).first(); + currentCountQuery: async (knex) => { + let result = await knex('members').count('id', {as: 'count'}).first(); return result.count; } }, newsletters: { - currentCountQuery: async (db) => { - let result = await db.knex('newsletters') + currentCountQuery: async (knex) => { + let result = await knex('newsletters') .count('id', {as: 'count'}) .where('status', '=', 'active') .first(); @@ -20,8 +20,8 @@ module.exports = { } }, emails: { - currentCountQuery: async (db, startDate) => { - let result = await db.knex('emails') + currentCountQuery: async (knex, startDate) => { + let result = await knex('emails') .sum('email_count', {as: 'count'}) .where('created_at', '>=', startDate) .first(); @@ -30,13 +30,13 @@ module.exports = { } }, staff: { - currentCountQuery: async (db) => { - let result = await db.knex('users') + currentCountQuery: async (knex) => { + let result = await knex('users') .select('users.id') .leftJoin('roles_users', 'users.id', 'roles_users.user_id') .leftJoin('roles', 'roles_users.role_id', 'roles.id') .whereNot('roles.name', 'Contributor').andWhereNot('users.status', 'inactive').union([ - db.knex('invites') + knex('invites') .select('invites.id') .leftJoin('roles', 'invites.role_id', 'roles.id') .whereNot('roles.name', 'Contributor') @@ -46,8 +46,8 @@ module.exports = { } }, customIntegrations: { - currentCountQuery: async (db) => { - let result = await db.knex('integrations') + currentCountQuery: async (knex) => { + let result = await knex('integrations') .count('id', {as: 'count'}) .whereNotIn('type', ['internal', 'builtin']) .first(); diff --git a/lib/limit-service.js b/lib/limit-service.js index 7ee9b95364f..13145c26667 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -67,13 +67,20 @@ class LimitService { return !!this.limits[_.camelCase(limitName)]; } - async checkIsOverLimit(limitName) { + /** + * + * @param {String} limitName - name of the configured limit + * @param {Object} [options] - limit parameters + * @param {Object} [options.transacting] Transaction to run the count query on (if required for the chosen limit) + * @returns + */ + async checkIsOverLimit(limitName, options = {}) { if (!this.isLimited(limitName)) { return; } try { - await this.limits[limitName].errorIfIsOverLimit(); + await this.limits[limitName].errorIfIsOverLimit(options); return false; } catch (error) { if (error instanceof this.errors.HostLimitError) { @@ -84,13 +91,20 @@ class LimitService { } } - async checkWouldGoOverLimit(limitName, metadata = {}) { + /** + * + * @param {String} limitName - name of the configured limit + * @param {Object} [options] - limit parameters + * @param {Object} [options.transacting] Transaction to run the count query on (if required for the chosen limit) + * @returns + */ + async checkWouldGoOverLimit(limitName, options = {}) { if (!this.isLimited(limitName)) { return; } try { - await this.limits[limitName].errorIfWouldGoOverLimit(metadata); + await this.limits[limitName].errorIfWouldGoOverLimit(options); return false; } catch (error) { if (error instanceof this.errors.HostLimitError) { @@ -104,39 +118,43 @@ class LimitService { /** * * @param {String} limitName - name of the configured limit - * @param {Object} metadata - limit parameters + * @param {Object} [options] - limit parameters + * @param {Object} [options.transacting] Transaction to run the count query on (if required for the chosen limit) * @returns */ - async errorIfIsOverLimit(limitName, metadata = {}) { + async errorIfIsOverLimit(limitName, options = {}) { if (!this.isLimited(limitName)) { return; } - await this.limits[limitName].errorIfIsOverLimit(metadata); + await this.limits[limitName].errorIfIsOverLimit(options); } /** * * @param {String} limitName - name of the configured limit - * @param {Object} metadata - limit parameters + * @param {Object} [options] - limit parameters + * @param {Object} [options.transacting] Transaction to run the count query on (if required for the chosen limit) * @returns */ - async errorIfWouldGoOverLimit(limitName, metadata = {}) { + async errorIfWouldGoOverLimit(limitName, options = {}) { if (!this.isLimited(limitName)) { return; } - await this.limits[limitName].errorIfWouldGoOverLimit(metadata); + await this.limits[limitName].errorIfWouldGoOverLimit(options); } /** * Checks if any of the configured limits acceded - * + * + * @param {Object} [options] - limit parameters + * @param {Object} [options.transacting] Transaction to run the count queries on (if required for the chosen limit) * @returns {Promise} */ - async checkIfAnyOverLimit() { + async checkIfAnyOverLimit(options = {}) { for (const limit in this.limits) { - if (await this.checkIsOverLimit(limit)) { + if (await this.checkIsOverLimit(limit, options)) { return true; } } diff --git a/lib/limit.js b/lib/limit.js index db2ae463e95..f2027459eae 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -98,8 +98,13 @@ class MaxLimit extends Limit { return new this.errors.HostLimitError(errorObj); } - async currentCountQuery() { - return await this.currentCountQueryFn(this.db); + /** + * @param {Object} [options] + * @param {Object} [options.transacting] Transaction to run the count query on + * @returns + */ + async currentCountQuery(options = {}) { + return await this.currentCountQueryFn(options.transacting ?? this.db?.knex); } /** @@ -108,9 +113,11 @@ class MaxLimit extends Limit { * @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 + * @param {Object} [options.transacting] Transaction to run the count query on */ - async errorIfWouldGoOverLimit({max, addedCount = 1} = {}) { - let currentCount = await this.currentCountQuery(); + async errorIfWouldGoOverLimit(options = {}) { + const {max, addedCount = 1} = options; + let currentCount = await this.currentCountQuery(options); if ((currentCount + addedCount) > (max || this.max)) { throw this.generateError(currentCount); @@ -123,11 +130,12 @@ class MaxLimit extends Limit { * @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 + * @param {Object} [options.transacting] Transaction to run the count query on */ - async errorIfIsOverLimit({max, currentCount} = {}) { - currentCount = currentCount || await this.currentCountQuery(); + async errorIfIsOverLimit(options = {}) { + const currentCount = options.currentCount || await this.currentCountQuery(options); - if (currentCount > (max || this.max)) { + if (currentCount > (options.max || this.max)) { throw this.generateError(currentCount); } } @@ -202,10 +210,15 @@ class MaxPeriodicLimit extends Limit { return new this.errors.HostLimitError(errorObj); } - async currentCountQuery() { + /** + * @param {Object} [options] + * @param {Object} [options.transacting] Transaction to run the count query on + * @returns + */ + async currentCountQuery(options = {}) { const lastPeriodStartDate = lastPeriodStart(this.startDate, this.interval); - return await this.currentCountQueryFn(this.db, lastPeriodStartDate); + return await this.currentCountQueryFn(options.transacting ? options.transacting : (this.db ? this.db.knex : undefined), lastPeriodStartDate); } /** @@ -214,9 +227,11 @@ class MaxPeriodicLimit extends Limit { * @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 + * @param {Object} [options.transacting] Transaction to run the count query on */ - async errorIfWouldGoOverLimit({max, addedCount = 1} = {}) { - let currentCount = await this.currentCountQuery(); + async errorIfWouldGoOverLimit(options = {}) { + const {max, addedCount = 1} = options; + let currentCount = await this.currentCountQuery(options); if ((currentCount + addedCount) > (max || this.maxPeriodic)) { throw this.generateError(currentCount); @@ -228,9 +243,11 @@ class MaxPeriodicLimit extends Limit { * * @param {Object} options * @param {Number} [options.max] - overrides configured default maxPeriodic value to perform checks against + * @param {Object} [options.transacting] Transaction to run the count query on */ - async errorIfIsOverLimit({max} = {}) { - let currentCount = await this.currentCountQuery(); + async errorIfIsOverLimit(options = {}) { + const {max} = options; + let currentCount = await this.currentCountQuery(options); if (currentCount > (max || this.maxPeriodic)) { throw this.generateError(currentCount); diff --git a/test/limit-service.test.js b/test/limit-service.test.js index 6340f443373..d2492b53155 100644 --- a/test/limit-service.test.js +++ b/test/limit-service.test.js @@ -4,6 +4,7 @@ require('./utils'); const should = require('should'); const LimitService = require('../lib/limit-service'); const {MaxLimit, MaxPeriodicLimit, FlagLimit} = require('../lib/limit'); +const sinon = require('sinon'); const errors = require('./fixtures/errors'); @@ -340,4 +341,174 @@ describe('Limit Service', function () { } }); }); + + describe('Metadata', function () { + afterEach(function () { + sinon.restore(); + }); + + it('passes options for checkIsOverLimit', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 1 + } + }; + + const maxSpy = sinon.spy(MaxLimit.prototype, 'errorIfIsOverLimit'); + + const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, errors, subscription}); + + const options = { + testData: 'true' + }; + + await limitService.checkIsOverLimit('staff', options); + + sinon.assert.callCount(maxSpy, 1); + sinon.assert.alwaysCalledWithExactly(maxSpy, options); + }); + + it('passes options for checkWouldGoOverLimit', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 1 + } + }; + + const maxSpy = sinon.spy(MaxLimit.prototype, 'errorIfWouldGoOverLimit'); + + const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, errors, subscription}); + + const options = { + testData: 'true' + }; + + await limitService.checkWouldGoOverLimit('staff', options); + + sinon.assert.callCount(maxSpy, 1); + sinon.assert.alwaysCalledWithExactly(maxSpy, options); + }); + + it('passes options for errorIfIsOverLimit', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 1 + } + }; + + const maxSpy = sinon.spy(MaxLimit.prototype, 'errorIfIsOverLimit'); + + const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, errors, subscription}); + + const options = { + testData: 'true' + }; + + await limitService.errorIfIsOverLimit('staff', options); + + sinon.assert.callCount(maxSpy, 1); + sinon.assert.alwaysCalledWithExactly(maxSpy, options); + }); + + it('passes options for errorIfWouldGoOverLimit', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 1 + } + }; + + const maxSpy = sinon.spy(MaxLimit.prototype, 'errorIfWouldGoOverLimit'); + + const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, errors, subscription}); + + const options = { + testData: 'true' + }; + + await limitService.errorIfWouldGoOverLimit('staff', options); + + sinon.assert.callCount(maxSpy, 1); + sinon.assert.alwaysCalledWithExactly(maxSpy, options); + }); + + it('passes options for checkIfAnyOverLimit', async function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 2 + }, + members: { + max: 100, + currentCountQuery: () => 100 + }, + emails: { + maxPeriodic: 3, + currentCountQuery: () => 3 + }, + customIntegrations: { + disabled: true + } + }; + + const flagSpy = sinon.spy(FlagLimit.prototype, 'errorIfIsOverLimit'); + const maxSpy = sinon.spy(MaxLimit.prototype, 'errorIfIsOverLimit'); + const maxPeriodSpy = sinon.spy(MaxPeriodicLimit.prototype, 'errorIfIsOverLimit'); + + const subscription = { + interval: 'month', + startDate: '2021-09-18T19:00:52Z' + }; + + limitService.loadLimits({limits, errors, subscription}); + + const options = { + testData: 'true' + }; + + (await limitService.checkIfAnyOverLimit(options)).should.be.false(); + + sinon.assert.callCount(flagSpy, 1); + sinon.assert.alwaysCalledWithExactly(flagSpy, options); + + sinon.assert.callCount(maxSpy, 2); + sinon.assert.alwaysCalledWithExactly(maxSpy, options); + + sinon.assert.callCount(maxPeriodSpy, 1); + sinon.assert.alwaysCalledWithExactly(maxPeriodSpy, options); + }); + }); }); diff --git a/test/limit.test.js b/test/limit.test.js index bd9e0209694..e75b178980f 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -2,6 +2,7 @@ // const testUtils = require('./utils'); require('./utils'); const should = require('should'); +const sinon = require('sinon'); const errors = require('./fixtures/errors'); const {MaxLimit, AllowlistLimit, FlagLimit, MaxPeriodicLimit} = require('../lib/limit'); @@ -278,6 +279,78 @@ describe('Limit Service', function () { } }); }); + + describe('Transactions', function () { + it('passes undefined if no db or transacting option passed', async function () { + const config = { + max: 5, + error: 'You have gone over the limit', + currentCountQuery: sinon.stub() + }; + + config.currentCountQuery.resolves(0); + + try { + const limit = new MaxLimit({name: '', config, errors}); + await limit.errorIfIsOverLimit(); + await limit.errorIfWouldGoOverLimit(); + } catch (error) { + should.fail('Should have not errored', error); + } + + sinon.assert.calledTwice(config.currentCountQuery); + sinon.assert.alwaysCalledWithExactly(config.currentCountQuery, undefined); + }); + + it('passes default db if no transacting option passed', async function () { + const config = { + max: 5, + error: 'You have gone over the limit', + currentCountQuery: sinon.stub() + }; + + const db = { + knex: 'This is our connection' + }; + config.currentCountQuery.resolves(0); + + try { + const limit = new MaxLimit({name: '', config, db, errors}); + await limit.errorIfIsOverLimit(); + await limit.errorIfWouldGoOverLimit(); + } catch (error) { + should.fail('Should have not errored', error); + } + + sinon.assert.calledTwice(config.currentCountQuery); + sinon.assert.alwaysCalledWithExactly(config.currentCountQuery, db.knex); + }); + + it('passes transacting option', async function () { + const config = { + max: 5, + error: 'You have gone over the limit', + currentCountQuery: sinon.stub() + }; + + const db = { + knex: 'This is our connection' + }; + const transaction = 'Our transaction'; + config.currentCountQuery.resolves(0); + + try { + const limit = new MaxLimit({name: '', config, db, errors}); + await limit.errorIfIsOverLimit({transacting: transaction}); + await limit.errorIfWouldGoOverLimit({transacting: transaction}); + } catch (error) { + should.fail('Should have not errored', error); + } + + sinon.assert.calledTwice(config.currentCountQuery); + sinon.assert.alwaysCalledWithExactly(config.currentCountQuery, transaction); + }); + }); }); describe('Periodic Max Limit', function () { @@ -490,6 +563,84 @@ describe('Limit Service', function () { } }); }); + + describe('Transactions', function () { + it('passes undefined if no db or transacting option passed', async function () { + const config = { + maxPeriodic: 5, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: sinon.stub() + }; + + config.currentCountQuery.resolves(0); + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, errors}); + await limit.errorIfIsOverLimit(); + await limit.errorIfWouldGoOverLimit(); + } catch (error) { + should.fail('Should have not errored', error); + } + + sinon.assert.calledTwice(config.currentCountQuery); + sinon.assert.alwaysCalledWith(config.currentCountQuery, undefined); + }); + + it('passes default db if no transacting option passed', async function () { + const config = { + maxPeriodic: 5, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: sinon.stub() + }; + + const db = { + knex: 'This is our connection' + }; + config.currentCountQuery.resolves(0); + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, db, errors}); + await limit.errorIfIsOverLimit(); + await limit.errorIfWouldGoOverLimit(); + } catch (error) { + should.fail('Should have not errored', error); + } + + sinon.assert.calledTwice(config.currentCountQuery); + sinon.assert.alwaysCalledWith(config.currentCountQuery, db.knex); + }); + + it('passes transacting option', async function () { + const config = { + maxPeriodic: 5, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: sinon.stub() + }; + + const db = { + knex: 'This is our connection' + }; + const transaction = 'Our transaction'; + config.currentCountQuery.resolves(0); + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, db, errors}); + await limit.errorIfIsOverLimit({transacting: transaction}); + await limit.errorIfWouldGoOverLimit({transacting: transaction}); + } catch (error) { + should.fail('Should have not errored', error); + } + + sinon.assert.calledTwice(config.currentCountQuery); + sinon.assert.alwaysCalledWith(config.currentCountQuery, transaction); + }); + }); }); describe('Allowlist limit', function () { From 5aad44d225913af32328e80c001b2917990718d5 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Thu, 12 May 2022 13:41:51 +0200 Subject: [PATCH 132/255] Published new versions - @tryghost/limit-service@1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4358d053644..5644ca8eaf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.1.3", + "version": "1.2.0", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From cc1d0aac6a37cd3f7e05ae2636aae19edb300cd6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 16 May 2022 04:56:38 +0000 Subject: [PATCH 133/255] Update dependency c8 to v7.11.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5644ca8eaf1..355740dc56f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.11.2", + "c8": "7.11.3", "mocha": "10.0.0", "should": "13.2.3", "sinon": "14.0.0" From 397163c65a8f326c15c4a85f53aa4bb28b1a769a Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 24 May 2022 13:06:28 +0200 Subject: [PATCH 134/255] Published new versions - @tryghost/adapter-manager@0.2.32 - @tryghost/api-version-compatibility-service@0.4.2 - @tryghost/bootstrap-socket@0.2.21 - @tryghost/config-url-helpers@1.0.1 - @tryghost/constants@1.0.6 - @tryghost/database-info@0.3.6 - @tryghost/email-content-generator@0.1.3 - @tryghost/image-transform@1.0.33 - @tryghost/job-manager@0.8.25 - @tryghost/limit-service@1.2.1 - @tryghost/minifier@0.1.16 - @tryghost/moleculer-service-from-class@0.2.27 - @tryghost/mw-api-version-mismatch@0.2.2 - @tryghost/mw-error-handler@1.0.2 - @tryghost/mw-session-from-token@0.1.33 - @tryghost/mw-update-user-last-seen@0.1.7 - @tryghost/package-json@1.0.22 - @tryghost/pretty-cli@1.2.28 - @tryghost/promise@0.1.19 - @tryghost/release-utils@0.8.0 - @tryghost/security@0.3.2 - @tryghost/session-service@0.1.43 - @tryghost/settings-path-manager@0.1.8 - @tryghost/version-notifications-data-service@0.2.1 - @tryghost/vhost-middleware@1.0.26 - @tryghost/zip@1.1.26 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 355740dc56f..e54ba017c55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.0", + "version": "1.2.1", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 74b92cee61c37d59458d5e8e2017b5bf4dc9e8c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:33:27 +0000 Subject: [PATCH 135/255] Update dependency c8 to v7.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e54ba017c55..00c4ea25e0b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.11.3", + "c8": "7.12.0", "mocha": "10.0.0", "should": "13.2.3", "sinon": "14.0.0" From 1f518b0158ed8218f6e15f812eabb3416893c34e Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 22 Jul 2022 16:46:56 +0100 Subject: [PATCH 136/255] Published new versions - @tryghost/adapter-manager@0.2.33 - @tryghost/api-version-compatibility-service@0.4.4 - @tryghost/bootstrap-socket@0.2.22 - @tryghost/config-url-helpers@1.0.2 - @tryghost/constants@1.0.7 - @tryghost/database-info@0.3.8 - @tryghost/email-content-generator@0.1.4 - @tryghost/image-transform@1.2.1 - @tryghost/job-manager@0.9.0 - @tryghost/limit-service@1.2.2 - @tryghost/minifier@0.1.17 - @tryghost/moleculer-service-from-class@0.2.28 - @tryghost/mw-api-version-mismatch@0.2.3 - @tryghost/mw-error-handler@1.0.5 - @tryghost/mw-session-from-token@0.1.34 - @tryghost/mw-update-user-last-seen@0.1.8 - @tryghost/package-json@1.0.23 - @tryghost/pretty-cli@1.2.29 - @tryghost/promise@0.1.20 - @tryghost/release-utils@0.8.1 - @tryghost/security@0.3.3 - @tryghost/session-service@0.1.44 - @tryghost/settings-path-manager@0.1.9 - @tryghost/version-notifications-data-service@0.2.2 - @tryghost/vhost-middleware@1.0.28 - @tryghost/zip@1.1.27 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 00c4ea25e0b..b7df05aa32c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.1", + "version": "1.2.2", "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 1608d8dd0f9c5c3545436c1d46fc766e37b242dd Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 26 Jul 2022 14:36:23 +0200 Subject: [PATCH 137/255] Updated repository links refs https://github.com/TryGhost/Toolbox/issues/354 - these packages have been moved from Utils so they need their links updating --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7df05aa32c..cda9ee0e3e7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/limit-service", "version": "1.2.2", - "repository": "https://github.com/TryGhost/Utils/tree/main/packages/limit-service", + "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", "main": "./lib/limit-service.js", From e5ac9abe60b7a95ec88ad2dd3d6555bbb6e6e326 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 5 Aug 2022 10:28:44 +0530 Subject: [PATCH 138/255] Published new versions - @tryghost/admin-api-schema@4.1.0 - @tryghost/admin-api@1.13.1 - @tryghost/color-utils@0.1.20 - @tryghost/config-url-helpers@1.0.3 - @tryghost/content-api@1.11.1 - @tryghost/helpers-gatsby@2.0.2 - @tryghost/helpers@1.1.72 - @tryghost/image-transform@1.2.2 - @tryghost/limit-service@1.2.3 - @tryghost/schema-org@0.1.30 - @tryghost/social-urls@0.1.33 - @tryghost/string@0.1.27 - @tryghost/timezone-data@0.2.71 - @tryghost/url-utils@4.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cda9ee0e3e7..41a1d009250 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.2", + "version": "1.2.3", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From ae42565b76602c27a0e0af274d67bce11619e625 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 16:58:36 +0000 Subject: [PATCH 139/255] Update dependency sinon to v14.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41a1d009250..0ac81fae725 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.12.0", "mocha": "10.0.0", "should": "13.2.3", - "sinon": "14.0.0" + "sinon": "14.0.1" }, "dependencies": { "@tryghost/errors": "^1.2.1", From c674e779e70cf317ea5758967961c7c7d871fafe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 02:00:14 +0000 Subject: [PATCH 140/255] Update dependency mocha to v10.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ac81fae725..462d1563353 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "7.12.0", - "mocha": "10.0.0", + "mocha": "10.1.0", "should": "13.2.3", "sinon": "14.0.1" }, From 9a4dfb6cb210f20b0ebafef6ed6ef01093ee2223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 03:17:34 +0000 Subject: [PATCH 141/255] Update Test & linting packages --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 462d1563353..313c873d574 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.12.0", "mocha": "10.1.0", "should": "13.2.3", - "sinon": "14.0.1" + "sinon": "14.0.2" }, "dependencies": { "@tryghost/errors": "^1.2.1", From d6547e7aba236af2c8d62c780d5d5478ed2d26be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 20:58:09 +0000 Subject: [PATCH 142/255] Update dependency sinon to v15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 313c873d574..84b19eceed9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.12.0", "mocha": "10.1.0", "should": "13.2.3", - "sinon": "14.0.2" + "sinon": "15.0.0" }, "dependencies": { "@tryghost/errors": "^1.2.1", From 5cdcc008de217adb8fe846219e1cb1bb7b5454ee Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Tue, 29 Nov 2022 15:09:39 +0000 Subject: [PATCH 143/255] Published new versions - @tryghost/adapter-base-cache@0.1.3 - @tryghost/admin-api-schema@4.2.1 - @tryghost/admin-api@1.13.2 - @tryghost/color-utils@0.1.22 - @tryghost/config-url-helpers@1.0.4 - @tryghost/content-api@1.11.5 - @tryghost/helpers-gatsby@2.0.5 - @tryghost/helpers@1.1.75 - @tryghost/image-transform@1.2.3 - @tryghost/limit-service@1.2.4 - @tryghost/schema-org@0.1.31 - @tryghost/social-urls@0.1.34 - @tryghost/string@0.2.2 - @tryghost/timezone-data@0.2.74 - @tryghost/url-utils@4.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84b19eceed9..0f90595364f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.3", + "version": "1.2.4", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 6fc4a86c6da5d68846152f856fe83bfc17856177 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 02:49:08 +0000 Subject: [PATCH 144/255] Update dependency mocha to v10.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f90595364f..3403183060a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "7.12.0", - "mocha": "10.1.0", + "mocha": "10.2.0", "should": "13.2.3", "sinon": "15.0.0" }, From 81d250b7993463109eef3a1715d49ad48915b91b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:25:28 +0000 Subject: [PATCH 145/255] Update dependency sinon to v15.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3403183060a..892454cf814 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.12.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.0" + "sinon": "15.0.1" }, "dependencies": { "@tryghost/errors": "^1.2.1", From 0208fc820ffe4201c44847708f0a3811cbcd9a2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 18:16:48 +0000 Subject: [PATCH 146/255] Update dependency c8 to v7.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 892454cf814..ad7aefdd855 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "7.12.0", + "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", "sinon": "15.0.1" From ac48a2b78038f63d64f8984445ed3000f0db9161 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 9 Mar 2023 09:30:30 +0100 Subject: [PATCH 147/255] Published new versions - @tryghost/adapter-base-cache@0.1.4 - @tryghost/admin-api-schema@4.2.2 - @tryghost/admin-api@1.13.3 - @tryghost/color-utils@0.1.23 - @tryghost/config-url-helpers@1.0.5 - @tryghost/content-api@1.11.6 - @tryghost/helpers-gatsby@2.0.6 - @tryghost/helpers@1.1.76 - @tryghost/image-transform@1.2.4 - @tryghost/limit-service@1.2.5 - @tryghost/schema-org@0.1.32 - @tryghost/social-urls@0.1.35 - @tryghost/string@0.2.3 - @tryghost/timezone-data@0.2.75 - @tryghost/url-utils@4.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad7aefdd855..4766ce88fd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.4", + "version": "1.2.5", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From f7e2dbaa98ffa02c629a971ed439beace46cd163 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 12:12:58 +0000 Subject: [PATCH 148/255] Update Test & linting packages --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4766ce88fd4..0ded65ac171 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.1" + "sinon": "15.0.2" }, "dependencies": { "@tryghost/errors": "^1.2.1", From 39df35aee93268134177c30b36e0efe44e308633 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 15 Mar 2023 13:49:24 +0000 Subject: [PATCH 149/255] Published new versions - @tryghost/adapter-base-cache@0.1.5 - @tryghost/admin-api-schema@4.2.3 - @tryghost/admin-api@1.13.4 - @tryghost/color-utils@0.1.24 - @tryghost/config-url-helpers@1.0.6 - @tryghost/content-api@1.11.7 - @tryghost/helpers-gatsby@2.0.7 - @tryghost/helpers@1.1.77 - @tryghost/image-transform@1.2.5 - @tryghost/limit-service@1.2.6 - @tryghost/schema-org@0.1.33 - @tryghost/social-urls@0.1.36 - @tryghost/string@0.2.4 - @tryghost/timezone-data@0.2.76 - @tryghost/url-utils@4.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ded65ac171..0b460f022fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.5", + "version": "1.2.6", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 403f8aa5ab16cd382b0c6c4a2ead191d2e3d5351 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 00:24:44 +0000 Subject: [PATCH 150/255] Update dependency sinon to v15.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b460f022fa..17943f2297f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.2" + "sinon": "15.0.3" }, "dependencies": { "@tryghost/errors": "^1.2.1", From ebfc93c2b8ce6551b6cb69027edb65d650f4377f Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 12 Apr 2023 13:26:04 +0200 Subject: [PATCH 151/255] Published new versions - @tryghost/adapter-base-cache@0.1.6 - @tryghost/admin-api-schema@4.2.4 - @tryghost/admin-api@1.13.5 - @tryghost/color-utils@0.1.25 - @tryghost/config-url-helpers@1.0.7 - @tryghost/content-api@1.11.8 - @tryghost/helpers-gatsby@2.0.8 - @tryghost/helpers@1.1.78 - @tryghost/image-transform@1.2.6 - @tryghost/limit-service@1.2.7 - @tryghost/schema-org@0.1.34 - @tryghost/social-urls@0.1.37 - @tryghost/string@0.2.5 - @tryghost/timezone-data@0.3.0 - @tryghost/url-utils@4.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17943f2297f..1946ddb6d1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.6", + "version": "1.2.7", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From a8d643aa3ca154d8198bcf8f84bd6a51297cf25f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 14:55:28 +0000 Subject: [PATCH 152/255] Update dependency sinon to v15.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1946ddb6d1c..ccf84c412e5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.3" + "sinon": "15.0.4" }, "dependencies": { "@tryghost/errors": "^1.2.1", From 2c8fe7f0220ea9b0df3c9d3cd91a028ab2926ebb Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 8 May 2023 10:47:45 +0200 Subject: [PATCH 153/255] Published new versions - @tryghost/adapter-base-cache@0.1.7 - @tryghost/admin-api-schema@4.3.1 - @tryghost/admin-api@1.13.6 - @tryghost/color-utils@0.1.26 - @tryghost/config-url-helpers@1.0.8 - @tryghost/content-api@1.11.10 - @tryghost/helpers-gatsby@2.0.10 - @tryghost/helpers@1.1.79 - @tryghost/image-transform@1.2.7 - @tryghost/limit-service@1.2.8 - @tryghost/schema-org@0.1.35 - @tryghost/social-urls@0.1.38 - @tryghost/string@0.2.6 - @tryghost/timezone-data@0.3.1 - @tryghost/url-utils@4.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ccf84c412e5..7f9987850e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.7", + "version": "1.2.8", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 18c38f897f3c02ead5e235eaef7f3a7762aabe7c Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Thu, 3 Aug 2023 20:46:15 +0100 Subject: [PATCH 154/255] 2023 --- LICENSE | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 19bcb01bef9..fc33a5ecee8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2013-2022 Ghost Foundation +Copyright (c) 2013-2023 Ghost Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 85caf91ae93..e48917f949d 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ const limits = { // The startDate has to be in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601) const subscription = { interval: 'month', - startDate: '2022-09-18T19:00:52Z' + startDate: '2023-09-18T19:00:52Z' }; // initialize the URL linking to help documentation etc. @@ -219,4 +219,4 @@ Follow the instructions for the top-level repo. # Copyright & License -Copyright (c) 2013-2022 Ghost Foundation - Released under the [MIT license](LICENSE). +Copyright (c) 2013-2023 Ghost Foundation - Released under the [MIT license](LICENSE). From c08660724ce680a7f19196c3a97153d9cb112842 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 8 Sep 2023 16:31:16 +0200 Subject: [PATCH 155/255] Bumped minimum version of `@tryghost/errors` - we decreased the bundle size at some point by changing the use of a subdependency, so bumping the requirement here means we can force it elsewhere too --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f9987850e3..9e30fe17a25 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "sinon": "15.0.4" }, "dependencies": { - "@tryghost/errors": "^1.2.1", + "@tryghost/errors": "^1.2.26", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 81edd56592fac87508de6b870cbef5828b2fe401 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 8 Sep 2023 16:34:39 +0200 Subject: [PATCH 156/255] Published new versions - @tryghost/adapter-base-cache@0.1.8 - @tryghost/admin-api-schema@4.5.2 - @tryghost/admin-api@1.13.8 - @tryghost/color-utils@0.1.27 - @tryghost/config-url-helpers@1.0.9 - @tryghost/content-api@1.11.16 - @tryghost/helpers-gatsby@2.0.16 - @tryghost/helpers@1.1.85 - @tryghost/image-transform@1.2.9 - @tryghost/limit-service@1.2.9 - @tryghost/schema-org@0.1.37 - @tryghost/social-urls@0.1.40 - @tryghost/string@0.2.8 - @tryghost/timezone-data@0.3.6 - @tryghost/url-utils@4.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e30fe17a25..d53f2bad3e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.8", + "version": "1.2.9", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 0ed9c2b50f582a5baa443f2bfe1daebf98b5719d Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 8 Sep 2023 17:19:20 +0200 Subject: [PATCH 157/255] Removed use of `lodash` import in favor of specific functions - this helps with tree-shaking because bundlers can leave out all the code that we don't use - the template + runInContext one was interesting but not too difficult because you can pass the `interpolate` pattern as a parameter to `template` - ideally we could reuse `@tryghost/tpl` but the interpolate string is different (1 bracket vs 2) --- lib/limit-service.js | 18 ++++++++++-------- lib/limit.js | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/limit-service.js b/lib/limit-service.js index 13145c26667..c4ece8ae6d5 100644 --- a/lib/limit-service.js +++ b/lib/limit-service.js @@ -1,7 +1,9 @@ +const camelCase = require('lodash/camelCase'); +const has = require('lodash/has'); +const {IncorrectUsageError} = require('@tryghost/errors'); + const {MaxLimit, MaxPeriodicLimit, FlagLimit, AllowlistLimit} = require('./limit'); const config = require('./config'); -const {IncorrectUsageError} = require('@tryghost/errors'); -const _ = require('lodash'); const messages = { missingErrorsConfig: `Config Missing: 'errors' is required.`, @@ -36,18 +38,18 @@ class LimitService { this.limits = {}; Object.keys(limits).forEach((name) => { - name = _.camelCase(name); + name = camelCase(name); // NOTE: config module acts as an allowlist of supported config names, where each key is a name of supported config if (config[name]) { /** @type LimitConfig */ let limitConfig = Object.assign({}, config[name], limits[name]); - if (_.has(limitConfig, 'allowlist')) { + if (has(limitConfig, 'allowlist')) { this.limits[name] = new AllowlistLimit({name, config: limitConfig, helpLink, errors}); - } else if (_.has(limitConfig, 'max')) { + } else if (has(limitConfig, 'max')) { this.limits[name] = new MaxLimit({name: name, config: limitConfig, helpLink, db, errors}); - } else if (_.has(limitConfig, 'maxPeriodic')) { + } else if (has(limitConfig, 'maxPeriodic')) { if (subscription === undefined) { throw new IncorrectUsageError({ message: messages.noSubscriptionParameter @@ -64,7 +66,7 @@ class LimitService { } isLimited(limitName) { - return !!this.limits[_.camelCase(limitName)]; + return !!this.limits[camelCase(limitName)]; } /** @@ -147,7 +149,7 @@ class LimitService { /** * Checks if any of the configured limits acceded - * + * * @param {Object} [options] - limit parameters * @param {Object} [options.transacting] Transaction to run the count queries on (if required for the chosen limit) * @returns {Promise} diff --git a/lib/limit.js b/lib/limit.js index f2027459eae..9d066387b12 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -1,8 +1,8 @@ -// run in context allows us to change the templateSettings without causing havoc -const _ = require('lodash').runInContext(); +const lowerCase = require('lodash/lowerCase'); +const template = require('lodash/template'); const {lastPeriodStart, SUPPORTED_INTERVALS} = require('./date-utils'); -_.templateSettings.interpolate = /{{([\s\S]+?)}}/g; +const interpolate = /{{([\s\S]+?)}}/g; class Limit { /** @@ -65,7 +65,7 @@ class MaxLimit extends Limit { 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.`; + this.fallbackMessage = `This action would exceed the ${lowerCase(this.name)} limit on your current plan.`; } /** @@ -81,7 +81,7 @@ class MaxLimit extends Limit { if (this.error) { const formatter = this.formatter || Intl.NumberFormat().format; try { - errorObj.message = _.template(this.error)( + errorObj.message = template(this.error, {interpolate})( { max: formatter(this.max), count: formatter(count), @@ -101,7 +101,7 @@ class MaxLimit extends Limit { /** * @param {Object} [options] * @param {Object} [options.transacting] Transaction to run the count query on - * @returns + * @returns */ async currentCountQuery(options = {}) { return await this.currentCountQueryFn(options.transacting ?? this.db?.knex); @@ -183,7 +183,7 @@ class MaxPeriodicLimit extends Limit { 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.`; + this.fallbackMessage = `This action would exceed the ${lowerCase(this.name)} limit on your current plan.`; } generateError(count) { @@ -193,7 +193,7 @@ class MaxPeriodicLimit extends Limit { if (this.error) { try { - errorObj.message = _.template(this.error)( + errorObj.message = template(this.error, {interpolate})( { max: Intl.NumberFormat().format(this.maxPeriodic), count: Intl.NumberFormat().format(count), @@ -213,7 +213,7 @@ class MaxPeriodicLimit extends Limit { /** * @param {Object} [options] * @param {Object} [options.transacting] Transaction to run the count query on - * @returns + * @returns */ async currentCountQuery(options = {}) { const lastPeriodStartDate = lastPeriodStart(this.startDate, this.interval); @@ -271,7 +271,7 @@ class FlagLimit extends Limit { 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)}.`; + this.fallbackMessage = `Your plan does not support ${lowerCase(this.name)}. Please upgrade to enable ${lowerCase(this.name)}.`; } generateError() { @@ -323,7 +323,7 @@ class AllowlistLimit extends Limit { } this.allowlist = config.allowlist; - this.fallbackMessage = `This action would exceed the ${_.lowerCase(this.name)} limit on your current plan.`; + this.fallbackMessage = `This action would exceed the ${lowerCase(this.name)} limit on your current plan.`; } generateError() { From a9aad75ad66b44517aa574c6db089ce2f0be7dd0 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 8 Sep 2023 17:22:31 +0200 Subject: [PATCH 158/255] Published new versions - @tryghost/limit-service@1.2.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d53f2bad3e2..4972ff88563 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.9", + "version": "1.2.10", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 5179fe1c9511951061776a98c08c208674fb7245 Mon Sep 17 00:00:00 2001 From: Jono M Date: Thu, 26 Oct 2023 10:34:54 +0100 Subject: [PATCH 159/255] Updated timezone-data package to use TypeScript (#480) refs https://github.com/TryGhost/Product/issues/4073 --- index.js | 1 + lib/{limit-service.js => LimitService.js} | 0 package.json | 4 ++-- test/{limit-service.test.js => LimitService.test.js} | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename lib/{limit-service.js => LimitService.js} (100%) rename test/{limit-service.test.js => LimitService.test.js} (99%) diff --git a/index.js b/index.js index e69de29bb2d..a1ad3df8780 100644 --- a/index.js +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/LimitService'); diff --git a/lib/limit-service.js b/lib/LimitService.js similarity index 100% rename from lib/limit-service.js rename to lib/LimitService.js diff --git a/package.json b/package.json index 4972ff88563..a1512640440 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", - "main": "./lib/limit-service.js", - "exports": "./lib/limit-service.js", + "main": "index.js", + "exports": "./index.js", "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", diff --git a/test/limit-service.test.js b/test/LimitService.test.js similarity index 99% rename from test/limit-service.test.js rename to test/LimitService.test.js index d2492b53155..cf97699711f 100644 --- a/test/limit-service.test.js +++ b/test/LimitService.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('./utils'); const should = require('should'); -const LimitService = require('../lib/limit-service'); +const LimitService = require('../lib/LimitService'); const {MaxLimit, MaxPeriodicLimit, FlagLimit} = require('../lib/limit'); const sinon = require('sinon'); From 15a76fceba344d99fcf36cd702bab489d694b174 Mon Sep 17 00:00:00 2001 From: Jono Mingard Date: Thu, 26 Oct 2023 11:02:40 +0100 Subject: [PATCH 160/255] Published new versions - @tryghost/adapter-base-cache@0.1.9 - @tryghost/admin-api@1.13.9 - @tryghost/content-api@1.11.18 - @tryghost/helpers-gatsby@2.0.18 - @tryghost/helpers@1.1.87 - @tryghost/limit-service@1.2.11 - @tryghost/string@0.2.9 - @tryghost/timezone-data@0.4.0 - @tryghost/url-utils@4.4.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1512640440..e51f41355fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.10", + "version": "1.2.11", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 9398a3b225d25ff055730017f382f8f381692b41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:03:38 +0000 Subject: [PATCH 161/255] Update Test & linting packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e51f41355fe..171f8145679 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "access": "public" }, "devDependencies": { - "c8": "7.13.0", + "c8": "7.14.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.4" + "sinon": "15.2.0" }, "dependencies": { "@tryghost/errors": "^1.2.26", From 210b956859f355a2c2dda36bac048f7e37c9f84b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:39:10 +0000 Subject: [PATCH 162/255] Update Test & linting packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 171f8145679..c54d7288f9d 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "access": "public" }, "devDependencies": { - "c8": "7.14.0", + "c8": "8.0.1", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.2.0" + "sinon": "17.0.0" }, "dependencies": { "@tryghost/errors": "^1.2.26", From de9648eb83ca7a6dcd84c39d5c88c45a4304e17f Mon Sep 17 00:00:00 2001 From: Jono Mingard Date: Mon, 30 Oct 2023 16:03:52 +0000 Subject: [PATCH 163/255] Published new versions - @tryghost/adapter-base-cache@0.1.10 - @tryghost/admin-api-schema@4.5.3 - @tryghost/admin-api@1.13.10 - @tryghost/color-utils@0.2.0 - @tryghost/config-url-helpers@1.0.10 - @tryghost/content-api@1.11.19 - @tryghost/helpers-gatsby@2.0.19 - @tryghost/helpers@1.1.88 - @tryghost/image-transform@1.2.10 - @tryghost/limit-service@1.2.12 - @tryghost/schema-org@0.1.38 - @tryghost/social-urls@0.1.41 - @tryghost/string@0.2.10 - @tryghost/timezone-data@0.4.1 - @tryghost/url-utils@4.4.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c54d7288f9d..e4a3c557b58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.11", + "version": "1.2.12", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 832448349be98101bde842048cc270d303783805 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:17:25 +0000 Subject: [PATCH 164/255] Update Test & linting packages --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e4a3c557b58..2b4c8c8fecc 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "8.0.1", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "17.0.0" + "sinon": "17.0.1" }, "dependencies": { "@tryghost/errors": "^1.2.26", From ac7dd8b2b8c1853f7c7ee9a618316c9462a39887 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 15 Nov 2023 12:41:56 -0600 Subject: [PATCH 165/255] Published new versions - @tryghost/adapter-base-cache@0.1.11 - @tryghost/admin-api-schema@4.5.4 - @tryghost/admin-api@1.13.11 - @tryghost/color-utils@0.2.1 - @tryghost/config-url-helpers@1.0.11 - @tryghost/content-api@1.11.20 - @tryghost/helpers-gatsby@2.0.20 - @tryghost/helpers@1.1.89 - @tryghost/image-transform@1.2.11 - @tryghost/limit-service@1.2.13 - @tryghost/schema-org@0.1.39 - @tryghost/social-urls@0.1.42 - @tryghost/string@0.2.11 - @tryghost/timezone-data@0.4.2 - @tryghost/url-utils@4.4.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b4c8c8fecc..4f2f7130693 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.12", + "version": "1.2.13", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From cb9b669c60d8fead5953e21ce6af8d823ef7e865 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 19:14:07 +0000 Subject: [PATCH 166/255] Update dependency c8 to v9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f2f7130693..be016911c22 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "8.0.1", + "c8": "9.0.0", "mocha": "10.2.0", "should": "13.2.3", "sinon": "17.0.1" From 713b8c20315fdb9db8847720f83b1f24fbb9c9aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:20:24 +0000 Subject: [PATCH 167/255] Update dependency c8 to v9.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be016911c22..36b257ebba9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "9.0.0", + "c8": "9.1.0", "mocha": "10.2.0", "should": "13.2.3", "sinon": "17.0.1" From ea9deec7cbd77e9cb846695229ab9db89ee61674 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 15:32:13 +0000 Subject: [PATCH 168/255] Update dependency mocha to v10.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36b257ebba9..e480abba066 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "9.1.0", - "mocha": "10.2.0", + "mocha": "10.3.0", "should": "13.2.3", "sinon": "17.0.1" }, From 224ffe11beac677d9b94ab74181b897f2d3b1d67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:49:29 +0000 Subject: [PATCH 169/255] Update dependency mocha to v10.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e480abba066..430b9c9a429 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "9.1.0", - "mocha": "10.3.0", + "mocha": "10.4.0", "should": "13.2.3", "sinon": "17.0.1" }, From ab56869ee2debe9c827f7113b9241708d261e9be Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Thu, 25 Apr 2024 19:29:23 +0100 Subject: [PATCH 170/255] Published new versions - @tryghost/adapter-base-cache@0.1.12 - @tryghost/admin-api-schema@4.5.5 - @tryghost/admin-api@1.13.12 - @tryghost/color-utils@0.2.2 - @tryghost/config-url-helpers@1.0.12 - @tryghost/content-api@1.11.21 - @tryghost/helpers-gatsby@2.0.21 - @tryghost/helpers@1.1.90 - @tryghost/image-transform@1.3.0 - @tryghost/limit-service@1.2.14 - @tryghost/schema-org@0.1.40 - @tryghost/social-urls@0.1.43 - @tryghost/string@0.2.12 - @tryghost/timezone-data@0.4.3 - @tryghost/url-utils@4.4.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 430b9c9a429..966e02c3a3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.13", + "version": "1.2.14", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 70190b5814cc341a0faa13aa068ad2fdd3ca4694 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 15:36:25 +0000 Subject: [PATCH 171/255] Update dependency sinon to v17.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 966e02c3a3e..f8b450b9299 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "9.1.0", "mocha": "10.4.0", "should": "13.2.3", - "sinon": "17.0.1" + "sinon": "17.0.2" }, "dependencies": { "@tryghost/errors": "^1.2.26", From cbec677e6827f9f60c2a0e5bdbf9b7295e11675c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 17:11:30 +0000 Subject: [PATCH 172/255] Update dependency sinon to v18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f8b450b9299..7f385829148 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "9.1.0", "mocha": "10.4.0", "should": "13.2.3", - "sinon": "17.0.2" + "sinon": "18.0.0" }, "dependencies": { "@tryghost/errors": "^1.2.26", From f7c2ce87202d66ba0ce1447a4a6ea83c602b5cd5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:16:27 +0000 Subject: [PATCH 173/255] Update dependency c8 to v10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f385829148..dbce31bb460 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "9.1.0", + "c8": "10.0.0", "mocha": "10.4.0", "should": "13.2.3", "sinon": "18.0.0" From 6c9bcd6003039ae1d6e89b7abe672a2e8ff9e9c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 00:58:28 +0000 Subject: [PATCH 174/255] Update dependency c8 to v10.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dbce31bb460..9c67c94729d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "10.0.0", + "c8": "10.1.1", "mocha": "10.4.0", "should": "13.2.3", "sinon": "18.0.0" From f8ae1d5aee7aef798ed7f139a0bf554d78005632 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 01:40:01 +0000 Subject: [PATCH 175/255] Update dependency c8 to v10.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c67c94729d..4f5b497443b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "10.1.1", + "c8": "10.1.2", "mocha": "10.4.0", "should": "13.2.3", "sinon": "18.0.0" From abc0c05ae94259afd1950680265169a19ee11bc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:47:38 +0000 Subject: [PATCH 176/255] Update dependency mocha to v10.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f5b497443b..af215a40d6b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.4.0", + "mocha": "10.5.0", "should": "13.2.3", "sinon": "18.0.0" }, From e1454d18f800179cfc97adcd6874bb228049fcd8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 07:43:30 +0000 Subject: [PATCH 177/255] Update dependency mocha to v10.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af215a40d6b..8cc02c5bcf7 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.5.0", + "mocha": "10.5.1", "should": "13.2.3", "sinon": "18.0.0" }, From e489c6fbec0f3bc91a46ba5ffc66681be46bff22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:26:10 +0000 Subject: [PATCH 178/255] Update dependency mocha to v10.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8cc02c5bcf7..c82bf904971 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.5.1", + "mocha": "10.5.2", "should": "13.2.3", "sinon": "18.0.0" }, From dafbe7558a88a8bd6a6a8f233410a9d96041e08f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 19:50:12 +0000 Subject: [PATCH 179/255] Update dependency mocha to v10.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c82bf904971..9b8064cb554 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.5.2", + "mocha": "10.6.0", "should": "13.2.3", "sinon": "18.0.0" }, From 86be9d61096551ea5a8500b1b2f08b88961396bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 04:36:32 +0000 Subject: [PATCH 180/255] Update Test & linting packages --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b8064cb554..5d846d4b97d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.6.0", + "mocha": "10.7.0", "should": "13.2.3", "sinon": "18.0.0" }, From 404854eb4e55b65d18f161a2d758ca00c1f00015 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:17:15 +0000 Subject: [PATCH 181/255] Update dependency mocha to v10.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d846d4b97d..5d290d3030c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.7.0", + "mocha": "10.7.3", "should": "13.2.3", "sinon": "18.0.0" }, From 9abcc9130b13d0c04ae1511b78de914ac57b387c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:19:39 +0000 Subject: [PATCH 182/255] Update dependency sinon to v18.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d290d3030c..384154cbcea 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "10.1.2", "mocha": "10.7.3", "should": "13.2.3", - "sinon": "18.0.0" + "sinon": "18.0.1" }, "dependencies": { "@tryghost/errors": "^1.2.26", From 5d1c8e595fc96186b8f79dc49c44e48bd5730fac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 03:28:50 +0000 Subject: [PATCH 183/255] Update dependency sinon to v19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 384154cbcea..b43be1fdd03 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "10.1.2", "mocha": "10.7.3", "should": "13.2.3", - "sinon": "18.0.1" + "sinon": "19.0.0" }, "dependencies": { "@tryghost/errors": "^1.2.26", From d61ae0056f5dc0e596974094443545f202c25df0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:28:20 +0000 Subject: [PATCH 184/255] Update dependency sinon to v19.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b43be1fdd03..5e7687d7766 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "10.1.2", "mocha": "10.7.3", "should": "13.2.3", - "sinon": "19.0.0" + "sinon": "19.0.2" }, "dependencies": { "@tryghost/errors": "^1.2.26", From 569cb2858b1a70f25304261ec08d973c1803c0e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:02:27 +0000 Subject: [PATCH 185/255] Update dependency mocha to v10.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e7687d7766..a838bd2f9cf 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.7.3", + "mocha": "10.8.1", "should": "13.2.3", "sinon": "19.0.2" }, From f1be24e29e046fddcca950984815e3028b3d149e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:03:59 +0000 Subject: [PATCH 186/255] Update dependency mocha to v10.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a838bd2f9cf..6c9ba2b34c4 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.8.1", + "mocha": "10.8.2", "should": "13.2.3", "sinon": "19.0.2" }, From c00419c4a498c342f251a06082a06f0bc81977af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:13:04 +0000 Subject: [PATCH 187/255] Update dependency mocha to v11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c9ba2b34c4..fb832da473c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.2", - "mocha": "10.8.2", + "mocha": "11.0.1", "should": "13.2.3", "sinon": "19.0.2" }, From 85acd7a5861f8f544309270785e9d05ca0593844 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:08:26 +0000 Subject: [PATCH 188/255] Update dependency c8 to v10.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb832da473c..73e8fa95ea4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "access": "public" }, "devDependencies": { - "c8": "10.1.2", + "c8": "10.1.3", "mocha": "11.0.1", "should": "13.2.3", "sinon": "19.0.2" From 30d6cd1739620a01697c2b9debc3d5038f6d3840 Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Mon, 6 Jan 2025 13:32:47 +0000 Subject: [PATCH 189/255] 2025 Co-authored-by: Hannah Wolfe github.erisds@gmail.com --- LICENSE | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index fc33a5ecee8..37c0d47d6f9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2013-2023 Ghost Foundation +Copyright (c) 2013-2025 Ghost Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e48917f949d..ff98c5c0d54 100644 --- a/README.md +++ b/README.md @@ -219,4 +219,4 @@ Follow the instructions for the top-level repo. # Copyright & License -Copyright (c) 2013-2023 Ghost Foundation - Released under the [MIT license](LICENSE). +Copyright (c) 2013-2025 Ghost Foundation - Released under the [MIT license](LICENSE). From e80a48df7b9ea1502376d420439024d9d50a934e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:24:24 +0000 Subject: [PATCH 190/255] Update dependency mocha to v11.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73e8fa95ea4..d36d8c4867e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.3", - "mocha": "11.0.1", + "mocha": "11.1.0", "should": "13.2.3", "sinon": "19.0.2" }, From 76793a8de29ee2867cd14fb2e30e6060a62135ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:54:07 +0000 Subject: [PATCH 191/255] Update dependency sinon to v19.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d36d8c4867e..4bd8f91eeaf 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "10.1.3", "mocha": "11.1.0", "should": "13.2.3", - "sinon": "19.0.2" + "sinon": "19.0.4" }, "dependencies": { "@tryghost/errors": "^1.2.26", From 6a051948cb82e1d3269f003040595b58d2eacc0c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:59:05 +0000 Subject: [PATCH 192/255] Update dependency sinon to v19.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4bd8f91eeaf..4628319b545 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "10.1.3", "mocha": "11.1.0", "should": "13.2.3", - "sinon": "19.0.4" + "sinon": "19.0.5" }, "dependencies": { "@tryghost/errors": "^1.2.26", From d8d206ad9ecc88d6929104163c2513068627bb61 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:32:33 +0000 Subject: [PATCH 193/255] Update dependency sinon to v20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4628319b545..fa57991bac1 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "c8": "10.1.3", "mocha": "11.1.0", "should": "13.2.3", - "sinon": "19.0.5" + "sinon": "20.0.0" }, "dependencies": { "@tryghost/errors": "^1.2.26", From e6cefc90f9e31c9858dda75e255d3f72a7255ae1 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Wed, 16 Apr 2025 14:52:48 +0900 Subject: [PATCH 194/255] Published new versions - @tryghost/adapter-base-cache@0.1.13 - @tryghost/admin-api-schema@4.5.6 - @tryghost/admin-api@1.13.13 - @tryghost/color-utils@0.2.3 - @tryghost/config-url-helpers@1.0.13 - @tryghost/content-api@1.11.22 - @tryghost/helpers-gatsby@2.0.22 - @tryghost/helpers@1.1.91 - @tryghost/image-transform@1.3.1 - @tryghost/limit-service@1.2.15 - @tryghost/schema-org@0.1.41 - @tryghost/social-urls@0.1.44 - @tryghost/string@0.2.13 - @tryghost/timezone-data@0.4.5 - @tryghost/url-utils@4.4.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa57991bac1..240d2d40774 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.14", + "version": "1.2.15", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From add9904fb0bb82608566c6ac809a913f8a30620a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 17:02:26 +0000 Subject: [PATCH 195/255] Update dependency mocha to v11.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 240d2d40774..8627df39eb4 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "c8": "10.1.3", - "mocha": "11.1.0", + "mocha": "11.2.2", "should": "13.2.3", "sinon": "20.0.0" }, From 3040041df7e9d608ff72689ff2ccdefae97e4e99 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 12 May 2025 15:29:17 +0200 Subject: [PATCH 196/255] Published new versions - @tryghost/adapter-base-cache@0.1.14 - @tryghost/admin-api-schema@4.5.7 - @tryghost/admin-api@1.13.14 - @tryghost/color-utils@0.2.4 - @tryghost/config-url-helpers@1.0.14 - @tryghost/content-api@1.11.23 - @tryghost/helpers-gatsby@2.0.23 - @tryghost/helpers@1.1.92 - @tryghost/html-to-plaintext@1.0.1 - @tryghost/image-transform@1.4.2 - @tryghost/limit-service@1.2.16 - @tryghost/members-csv@2.0.1 - @tryghost/referrer-parser@0.1.3 - @tryghost/schema-org@0.1.42 - @tryghost/social-urls@0.1.48 - @tryghost/string@0.2.14 - @tryghost/timezone-data@0.4.6 - @tryghost/url-utils@4.4.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8627df39eb4..a4d2a58b4ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.15", + "version": "1.2.16", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 18ccd1c2a19072c892dc57182709f032cf230840 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 12 May 2025 15:44:05 +0200 Subject: [PATCH 197/255] Published new versions - @tryghost/adapter-base-cache@0.1.15 - @tryghost/admin-api-schema@4.5.8 - @tryghost/admin-api@1.13.15 - @tryghost/color-utils@0.2.5 - @tryghost/config-url-helpers@1.0.15 - @tryghost/content-api@1.11.24 - @tryghost/helpers-gatsby@2.0.24 - @tryghost/helpers@1.1.93 - @tryghost/html-to-plaintext@1.0.2 - @tryghost/image-transform@1.4.3 - @tryghost/limit-service@1.2.17 - @tryghost/members-csv@2.0.2 - @tryghost/referrer-parser@0.1.4 - @tryghost/schema-org@0.1.43 - @tryghost/social-urls@0.1.49 - @tryghost/string@0.2.15 - @tryghost/timezone-data@0.4.7 - @tryghost/url-utils@4.4.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4d2a58b4ef..7de5dbfcbd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.16", + "version": "1.2.17", "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", "author": "Ghost Foundation", "license": "MIT", From 75590f1c4d14136a9c1ac4f0cdd7efabe289a96b Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sun, 18 May 2025 12:45:22 +0100 Subject: [PATCH 198/255] Updated repository in package.json - This is a more modern / up-to-date structure for repository - It's correct according to package.json docs (unlike using a URL directly) - I'm updating this where I find it - for consistency --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7de5dbfcbd4..6b564f578e2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,11 @@ { "name": "@tryghost/limit-service", "version": "1.2.17", - "repository": "https://github.com/TryGhost/SDK/tree/main/packages/limit-service", + "repository": { + "type": "git", + "url": "git+https://github.com/TryGhost/SDK.git", + "directory": "packages/limit-service" + }, "author": "Ghost Foundation", "license": "MIT", "main": "index.js", From d928f81d28b612da3a0d54fe1a8c474fb9cf0217 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sun, 25 May 2025 21:46:25 +0100 Subject: [PATCH 199/255] Published new versions - @tryghost/adapter-base-cache@0.1.16 - @tryghost/admin-api-schema@4.5.9 - @tryghost/admin-api@1.13.16 - @tryghost/color-utils@0.2.6 - @tryghost/config-url-helpers@1.0.16 - @tryghost/content-api@1.11.25 - @tryghost/custom-fonts@1.0.1 - @tryghost/helpers-gatsby@2.0.25 - @tryghost/helpers@1.1.94 - @tryghost/html-to-plaintext@1.0.4 - @tryghost/image-transform@1.4.4 - @tryghost/limit-service@1.2.18 - @tryghost/members-csv@2.0.3 - @tryghost/referrer-parser@0.1.5 - @tryghost/schema-org@0.1.44 - @tryghost/social-urls@0.1.50 - @tryghost/string@0.2.16 - @tryghost/timezone-data@0.4.8 - @tryghost/url-utils@4.4.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b564f578e2..d9b8af3e294 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.17", + "version": "1.2.18", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 94d3b4578d2dfa71bdeb514b0d682dfe707cd8e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 07:47:54 +0000 Subject: [PATCH 200/255] Update dependency sinon to v21 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9b8af3e294..2e1f3034265 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "10.1.3", "mocha": "11.2.2", "should": "13.2.3", - "sinon": "20.0.0" + "sinon": "21.0.0" }, "dependencies": { "@tryghost/errors": "^1.2.26", From 18422404a4feb940528eeaf18db17cedee475b0c Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Wed, 25 Jun 2025 09:54:55 +0400 Subject: [PATCH 201/255] Removed `currentCountQuery` from limit service config for `customIntegrations` ref BAE-331 ref https://ghost.slack.com/archives/C02G9E68C/p1750776240012249 Since the introduction of the `customIntegrations` limit, it was been treated as a `Flag` limit, which is a simple on/off limit. The query that was assigned to in the config has never been executed and is not found in use anywhere in our repos. To be able to let customers use a feature, which they have enabled, after we impose a limit for that feature, we're exploring the option to use `currentCountQuery` to overwrite the imposed limit if the feature is already being used. Having a query in the config for custom integrations messes with that idea, besides the fact that it's a pointless query. --- README.md | 14 +++++++------- lib/config.js | 11 +---------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ff98c5c0d54..8c32bd88840 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This module is intended to hold **all of the logic** for testing if site: - would be over a given limit if they took an action (i.e. added one more thing, switched to a different limit) - if they are over a limit already -- consistent error messages explaining why the limit has been reached +- consistent error messages explaining why the limit has been reached ## Install @@ -54,7 +54,7 @@ const limits = { error: 'Email sending has been temporarily disabled whilst your account is under review.' }, // following is a "max periodic" type of configuration - // note if you use this configuration, the limit service has to also get a + // note if you use this configuration, the limit service has to also get a // "subscription" parameter to work as expected // emails: { // maxPeriodic: 42, @@ -69,7 +69,7 @@ const limits = { }; // This information is needed for the limit service to work with "max periodic" limits -// The interval value has to be 'month' as thats the only interval that was needed for +// The interval value has to be 'month' as that's the only interval that was needed for // current usecase // The startDate has to be in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601) const subscription = { @@ -155,12 +155,12 @@ db.transaction((transacting) => { ### Types of limits At the moment there are four different types of limits that limit service allows to define. These types are: 1. `flag` - is an "on/off" switch for certain feature. Example usecase: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. -2. `max` - checks if the maximum amount of the resource has been used up.Example usecase: "disable creating a staff user when maximum of 5 has been reached". To configure this limit add `max: NUMBER` to the configuration. The limits that support max checks are: `members`, `staff`, and `customIntegrations` +2. `max` - checks if the maximum amount of the resource has been used up.Example usecase: "disable creating a staff user when maximum of 5 has been reached". To configure this limit add `max: NUMBER` to the configuration. The limits that support max checks are: `members`, and `staff` 3. `maxPeriodic` - it's a variation of `max` type with a difference that the check is done over certain period of time. Example usecase: "disable sending emails when the sent emails count has acceded a limit for last billing period". To enable this limit define `maxPeriodic: NUMBER` in the limit configuration and provide a subscription configuration when initializing the limit service instance. The subscription object comes as a separate parameter and has to contain two properties: `startDate` and `interval`, where `startDate` is a date in ISO 8601 format and period is `'month'` (other values like `'year'` are not supported yet) -4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. +4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. ### Supported limits -There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`, `uploads`. +There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`, `uploads`. All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff`, and`customIntegrations`) can have a `max` limit. Only `emails` currently supports the `maxPeriodic` type of limit. @@ -217,6 +217,6 @@ Follow the instructions for the top-level repo. -# Copyright & License +# Copyright & License Copyright (c) 2013-2025 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/lib/config.js b/lib/config.js index 4166a60298c..5a87266a141 100644 --- a/lib/config.js +++ b/lib/config.js @@ -45,16 +45,7 @@ module.exports = { return result.length; } }, - customIntegrations: { - currentCountQuery: async (knex) => { - let result = await knex('integrations') - .count('id', {as: 'count'}) - .whereNotIn('type', ['internal', 'builtin']) - .first(); - - return result.count; - } - }, + customIntegrations: {}, customThemes: {}, uploads: { // NOTE: this function should not ever be used as for uploads we compare the size From e6650e6c09280549e6600999e3dbcbccde8ec081 Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Wed, 25 Jun 2025 10:21:49 +0400 Subject: [PATCH 202/255] Added new limits to Limit-Service config (#539) closes BAE-331 We're introducing 3 new limits, which Ghost need to be aware of through the Limit-Service: - `limitStripeConnect` - `limitAnalytics` - `limitActivityPub` This adds those limits to the config, allowing them to be passed through to Ghost and being available for limit checks. All of the new limits are `Flag` type limits and don't require a `currentCountQuery`. --- README.md | 12 ++++++++---- lib/config.js | 5 ++++- lib/limit.js | 3 ++- test/limit.test.js | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8c32bd88840..31f3dda9673 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ const LimitService = require('@tryghost/limit-service'); const limitService = new LimitService(); // setup limit configuration -// currently supported limit keys are: staff, members, customThemes, customIntegrations, uploads +// currently supported limit keys are: staff, members, customThemes, customIntegrations, uploads, +// limitStripeConnect, limitAnalytics, and limitActivityPub // all limit configs support custom "error" configuration that is a template string const limits = { // staff and member are "max" type of limits accepting "max" configuration @@ -65,7 +66,10 @@ const limits = { max: 5000000, // formatting of the {{ max }} vairable is in MB, e.g: 5MB error: 'Your plan supports uploads of max size up to {{max}}. Please upgrade to reenable uploading.' - } + }, + limitStripeConnect: {}, + limitAnalytics: {}, + limitActivityPub: {} }; // This information is needed for the limit service to work with "max periodic" limits @@ -160,9 +164,9 @@ At the moment there are four different types of limits that limit service allows 4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. ### Supported limits -There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`, `uploads`. +There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`, `uploads`, `limitStripeConnect`, `limitAnalytics`, and `limitActivityPub`. -All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff`, and`customIntegrations`) can have a `max` limit. Only `emails` currently supports the `maxPeriodic` type of limit. +All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff`) can have a `max` limit. Only `emails` currently supports the `maxPeriodic` type of limit. ### Frontend usage In case the limit check is run without direct access to the database you can override `currentCountQuery` functions for each "max" or "maxPeriodic" type of limit. An example usecase would be a frontend client running in a browser. A browser client can check the limit data through HTTP request and then provide that data to the limit service. Example code to do exactly that: diff --git a/lib/config.js b/lib/config.js index 5a87266a141..63d5103604a 100644 --- a/lib/config.js +++ b/lib/config.js @@ -55,5 +55,8 @@ module.exports = { // NOTE: the uploads limit is based on file sizes provided in Bytes // a custom formatter is here for more user-friendly formatting when forming an error formatter: count => `${count / 1000000}MB` - } + }, + limitStripeConnect: {}, + limitAnalytics: {}, + limitActivityPub: {} }; diff --git a/lib/limit.js b/lib/limit.js index 9d066387b12..363f39ac203 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -269,9 +269,10 @@ class FlagLimit extends Limit { */ constructor({name, config, helpLink, db, errors}) { super({name, error: config.error || '', helpLink, db, errors}); + const userFacingLimitName = lowerCase(name.replace(/^limit/, '')); this.disabled = config.disabled; - this.fallbackMessage = `Your plan does not support ${lowerCase(this.name)}. Please upgrade to enable ${lowerCase(this.name)}.`; + this.fallbackMessage = `Your plan does not support ${userFacingLimitName}. Please upgrade to enable ${userFacingLimitName}.`; } generateError() { diff --git a/test/limit.test.js b/test/limit.test.js index e75b178980f..96543a9865d 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -25,7 +25,7 @@ describe('Limit Service', function () { const config = { disabled: true }; - const limit = new FlagLimit({name: 'flaggy', config, errors}); + const limit = new FlagLimit({name: 'limitFlaggy', config, errors}); try { await limit.errorIfWouldGoOverLimit(); @@ -37,7 +37,7 @@ describe('Limit Service', function () { should.equal(err.errorType, 'HostLimitError'); should.exist(err.errorDetails); - should.equal(err.errorDetails.name, 'flaggy'); + should.equal(err.errorDetails.name, 'limitFlaggy'); should.exist(err.message); should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.'); From 4f31cdd71ba3349ec691e1ae384108be3e658d6d Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Wed, 25 Jun 2025 12:26:01 +0400 Subject: [PATCH 203/255] Added CLAUDE.md file for AI assistant guidance This commit introduces a CLAUDE.md file to help AI assistants understand and work effectively with the limit-service codebase. The file was missing, making it difficult for AI tools to quickly grasp the architecture and development workflow. The CLAUDE.md file provides: - Common development commands for testing and linting - High-level architecture overview of the limit enforcement system - Clear guidance on adding new limits and understanding the codebase structure - Testing approach and database integration patterns Future AI assistant interactions will be more efficient as they can reference this file to understand the codebase structure, run appropriate commands, and follow established patterns when making changes to the limit-service package. --- CLAUDE.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..f813b73524c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Development Commands + +### Testing +- **Run all tests with coverage**: `npm test` +- **Run a specific test file**: `NODE_ENV=testing mocha './test/limit.test.js'` +- **Run tests matching a pattern**: `NODE_ENV=testing mocha './test/**/*.test.js' --grep "MaxLimit"` +- **Run tests with coverage report**: `NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'` + +### Linting +- **Run ESLint**: `npm run lint` +- **Fix linting issues**: `npm run lint -- --fix` + +### Development +- **Note**: There is no dev script currently implemented (placeholder exists) + +## High-Level Architecture + +The limit-service is a centralized limit enforcement system for Ghost that follows clean architecture principles: + +### Core Components + +1. **LimitService** (`lib/LimitService.js`): Main service class that acts as a facade for all limit operations. It creates and manages different limit types based on configuration. + +2. **Limit Types** (`lib/limit.js`): + - **MaxLimit**: Enforces maximum counts (e.g., max 5 staff users) + - **MaxPeriodicLimit**: Enforces limits over time periods (e.g., max emails per month) + - **FlagLimit**: On/off feature toggles + - **AllowlistLimit**: Restricts values to an allowed list + +3. **Configuration** (`lib/config.js`): Defines supported limits and their properties. Each limit can specify: + - `type`: The limit type to use + - `fallbackErrorMessage`: Default error message + - `currentCountQuery`: Database query function for count-based limits + +### Key Architectural Patterns + +- **Strategy Pattern**: Different limit types implement a common interface (`checkIsOverLimit`, `checkWouldGoOverLimit`) +- **Dependency Injection**: Database connection, errors handler, and configuration are injected at initialization +- **Transaction Support**: All database operations can be wrapped in transactions via `options.transacting` + +### Adding New Limits + +1. Add the limit configuration in `lib/config.js`: + ```javascript + newFeature: { + type: 'max', // or 'flag', 'allowlist' + fallbackErrorMessage: 'Default error for {{name}} limit', + currentCountQuery: async (db, options) => { + // Return current count from database + } + } + ``` + +2. Test the new limit following existing patterns in `test/` + +### Testing Approach + +- Uses Mocha with Should.js for assertions +- Sinon for mocking database queries and date/time +- Tests focus on behavior, not implementation +- Mock database responses to test limit logic in isolation + +### Database Integration + +- Expects a Knex instance for database queries +- All queries support transactions +- Count queries should return a number or be convertible to a number +- For periodic limits, queries receive `startDate` and `endDate` parameters + +### Error Handling + +- Uses `@tryghost/errors` for consistent error formatting +- Supports template variables in error messages: `{{max}}`, `{{count}}`, `{{name}}` +- All limits have fallback error messages +- Numbers in error messages are formatted with toLocaleString() + +### Key Methods Flow + +1. `loadLimits()`: Initializes the service with configuration +2. `isLimited()`: Checks if a limit is configured +3. `errorIfWouldGoOverLimit()`: Throws if action would exceed limit +4. `errorIfIsOverLimit()`: Throws if already over limit +5. `checkIsOverLimit()`: Returns boolean for limit status +6. `checkWouldGoOverLimit()`: Returns boolean for potential limit breach + +### Environment Variable + +- Set `NODE_ENV=testing` when running tests \ No newline at end of file From f2653ab6a2c374bad584bbd557bfd2ae860fb9a2 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 25 Jun 2025 09:02:21 -0500 Subject: [PATCH 204/255] Published new versions - @tryghost/adapter-base-cache@0.1.17 - @tryghost/admin-api-schema@4.5.10 - @tryghost/admin-api@1.13.17 - @tryghost/color-utils@0.2.7 - @tryghost/config-url-helpers@1.0.17 - @tryghost/content-api@1.11.27 - @tryghost/custom-fonts@1.0.2 - @tryghost/helpers-gatsby@2.0.27 - @tryghost/helpers@1.1.96 - @tryghost/image-transform@1.4.6 - @tryghost/limit-service@1.2.19 - @tryghost/referrer-parser@0.1.8 - @tryghost/schema-org@0.1.45 - @tryghost/social-urls@0.1.51 - @tryghost/string@0.2.17 - @tryghost/timezone-data@0.4.9 - @tryghost/url-utils@4.4.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e1f3034265..a03ba6ba1a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.18", + "version": "1.2.19", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 165a5e0ec315efad5f44e121f4698541374f2f32 Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Wed, 25 Jun 2025 12:29:07 +0400 Subject: [PATCH 205/255] Added currentCountQuery support for flag limits to grandfather usage ref BAE-336 ref BAE-330 The flag limits were previously simple on/off switches that would disable features entirely when enabled. This would cause issues when introducing new limits to existing plans where customers are already using the affected features, as it would immediately block their access upon implementation of new pricing. This commit adds support for a currentCountQuery function to flag limits, allowing them to check if a feature is already in use. When a feature is detected as being in use, the limit won't be enforced, effectively grandfathering existing usage. The limitAnalytics limit now includes this query to check both settings and labs flags for the trafficAnalytics feature. This change ensures that existing customers who are already using features won't lose access when new limits are introduced with the upcoming pricing changes, improving the user experience during plan transitions while still enforcing limits for new feature adoption. --- README.md | 2 +- lib/config.js | 13 +++++- lib/limit.js | 53 +++++++++++++++++++--- test/LimitService.test.js | 53 ++++++++++++++++++---- test/limit.test.js | 92 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 191 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 31f3dda9673..7b2657823e4 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ db.transaction((transacting) => { ### Types of limits At the moment there are four different types of limits that limit service allows to define. These types are: -1. `flag` - is an "on/off" switch for certain feature. Example usecase: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. +1. `flag` - is an "on/off" switch for certain feature. Example usecase: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. It is possible to overwrite the limit by providing a `currentCountQuery` for it. This is useful in cases where we introduce new limits to existing plans and customers have already been using the feature affected by the limit. By providing a `currentCountQuery` that detects if the feature is already in use, we won't disable it. 2. `max` - checks if the maximum amount of the resource has been used up.Example usecase: "disable creating a staff user when maximum of 5 has been reached". To configure this limit add `max: NUMBER` to the configuration. The limits that support max checks are: `members`, and `staff` 3. `maxPeriodic` - it's a variation of `max` type with a difference that the check is done over certain period of time. Example usecase: "disable sending emails when the sent emails count has acceded a limit for last billing period". To enable this limit define `maxPeriodic: NUMBER` in the limit configuration and provide a subscription configuration when initializing the limit service instance. The subscription object comes as a separate parameter and has to contain two properties: `startDate` and `interval`, where `startDate` is a date in ISO 8601 format and period is `'month'` (other values like `'year'` are not supported yet) 4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. diff --git a/lib/config.js b/lib/config.js index 63d5103604a..81d9da5f464 100644 --- a/lib/config.js +++ b/lib/config.js @@ -57,6 +57,17 @@ module.exports = { formatter: count => `${count / 1000000}MB` }, limitStripeConnect: {}, - limitAnalytics: {}, + limitAnalytics: { + currentCountQuery: async (knex) => { + const key = 'trafficAnalytics'; + const settings = await knex('settings'); + const setting = settings.find(s => s.key === key); + const labSettings = settings.find(s => s.key === 'labs'); + const labsSetting = labSettings && labSettings.value && labSettings.value[key]; + const labsSettingEnabled = labsSetting === true; + + return (setting && setting.value === 'true') || labsSettingEnabled; + } + }, limitActivityPub: {} }; diff --git a/lib/limit.js b/lib/limit.js index 363f39ac203..e7fbfdf4dc1 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -264,6 +264,7 @@ class FlagLimit extends Limit { * @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 {Function} [options.config.currentCountQuery] - query checking the state that would be compared against the limit * @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) */ @@ -273,6 +274,7 @@ class FlagLimit extends Limit { this.disabled = config.disabled; this.fallbackMessage = `Your plan does not support ${userFacingLimitName}. Please upgrade to enable ${userFacingLimitName}.`; + this.currentCountQueryFn = config?.currentCountQuery || null; } generateError() { @@ -288,20 +290,59 @@ class FlagLimit extends Limit { } /** - * Flag limits are on/off so using a feature is always over the limit + * @param {Object} [options] + * @param {Object} [options.transacting] Transaction to run the count query on + * @returns {Promise} - returns the current count of items that would be compared against the limit */ - async errorIfWouldGoOverLimit() { - if (this.disabled) { + async currentCountQuery(options = {}) { + if (!this.currentCountQueryFn || typeof this.currentCountQueryFn !== 'function') { + return false; + } + + return await this.currentCountQueryFn(options.transacting ?? this.db?.knex); + } + + // As Flag limits are on/off, we won't check against max values. + // `errorIfWouldGoOverLimit` and `errorIfIsOverLimit` end up doing the same thing. + async _isOrWouldOverLimitError(options = {}) { + if (!this.disabled) { + return; + } + + // If no currentCountQuery is provided, throw error when disabled + if (!this.currentCountQueryFn || typeof this.currentCountQueryFn !== 'function') { throw this.generateError(); } + + // If currentCountQuery is provided, check if feature is in use + const featureInUse = await this.currentCountQuery(options); + + // Only throw error if feature is NOT in use (allowing grandfathering) + if (!featureInUse) { + throw this.generateError(); + } + } + + /** + * Flag limits are usually on/off so using a feature is always over the limit, + * unless the limit has a currentCountQuery function provided to check if the + * feature is in use. This is a use case for when we introduce a new limit and + * customers have already been using this feature. We don't want to take it + * away from them. + */ + async errorIfWouldGoOverLimit(options = {}) { + await this._isOrWouldOverLimitError(options); } /** * 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! + * Exception: the limit has a currentCountQuery function provided to check if the + * feature is in use. This is a use case for when we introduce a new limit and + * customers have already been using this feature. We don't want to take it + * away from them. */ - async errorIfIsOverLimit() { - return; + async errorIfIsOverLimit(options = {}) { + await this._isOrWouldOverLimitError(options); } } diff --git a/test/LimitService.test.js b/test/LimitService.test.js index cf97699711f..b33ed1bfd2c 100644 --- a/test/LimitService.test.js +++ b/test/LimitService.test.js @@ -131,17 +131,24 @@ describe('Limit Service', function () { let limits = { staff: {max: 2}, members: {max: 100}, - emails: {disabled: true} + emails: {disabled: true}, + limitStripeConnect: {disabled: true}, + limitActivityPub: {disabled: true} }; limitService.loadLimits({limits, errors}); - limitService.limits.should.be.an.Object().with.properties(['staff', 'members']); + limitService.limits.should.be.an.Object().with.properties(['staff', 'members', 'emails', 'limitStripeConnect', 'limitActivityPub']); limitService.limits.staff.should.be.an.instanceOf(MaxLimit); limitService.limits.members.should.be.an.instanceOf(MaxLimit); + limitService.limits.emails.should.be.an.instanceOf(FlagLimit); + limitService.limits.limitStripeConnect.should.be.an.instanceOf(FlagLimit); + limitService.limits.limitActivityPub.should.be.an.instanceOf(FlagLimit); limitService.isLimited('staff').should.be.true(); limitService.isLimited('members').should.be.true(); limitService.isLimited('emails').should.be.true(); + limitService.isLimited('limitStripeConnect').should.be.true(); + limitService.isLimited('limitActivityPub').should.be.true(); }); it('can load camel cased limits', function () { @@ -255,6 +262,12 @@ describe('Limit Service', function () { }, customIntegrations: { disabled: true + }, + limitStripeConnect: { + disabled: true + }, + limitActivityPub: { + disabled: true } }; @@ -268,7 +281,7 @@ describe('Limit Service', function () { (await limitService.checkIfAnyOverLimit()).should.be.true(); }); - it('Does not confirm if no limits are acceded', async function () { + it('Confirms when a flag limit without currentCountQuery is disabled', async function () { const limitService = new LimitService(); let limits = { @@ -288,10 +301,21 @@ describe('Limit Service', function () { // customThemes: { // allowlist: ['casper', 'dawn', 'lyra'] // }, - // NOTE: the flag limit has flawed assumption of not being acceded previously - // this test might fail when the flaw is addressed customIntegrations: { disabled: true + // No currentCountQuery - will be considered over limit + }, + limitAnalytics: { + disabled: true, + currentCountQuery: () => true // Feature is in use, so limit won't be exceeded (grandfathered) + }, + limitStripeConnect: { + disabled: true + // No currentCountQuery - will be considered over limit + }, + limitActivityPub: { + disabled: true + // No currentCountQuery - will be considered over limit } }; @@ -302,7 +326,8 @@ describe('Limit Service', function () { limitService.loadLimits({limits, errors, subscription}); - (await limitService.checkIfAnyOverLimit()).should.be.false(); + // Should return true because customIntegrations is disabled without currentCountQuery + (await limitService.checkIfAnyOverLimit()).should.be.true(); }); it('Returns nothing if limit is not configured', async function () { @@ -480,7 +505,17 @@ describe('Limit Service', function () { currentCountQuery: () => 3 }, customIntegrations: { - disabled: true + disabled: false // Not disabled, so won't be over limit + }, + limitAnalytics: { + disabled: true, + currentCountQuery: () => true // Feature is in use, so limit won't be exceeded (grandfathered) + }, + limitStripeConnect: { + disabled: false // Not disabled, so won't be over limit + }, + limitActivityPub: { + disabled: false // Not disabled, so won't be over limit } }; @@ -499,9 +534,11 @@ describe('Limit Service', function () { testData: 'true' }; + // Should return false because no limits are exceeded (await limitService.checkIfAnyOverLimit(options)).should.be.false(); - sinon.assert.callCount(flagSpy, 1); + // We have 4 flag limits now: customIntegrations, limitAnalytics, limitStripeConnect, and limitActivityPub + sinon.assert.callCount(flagSpy, 4); sinon.assert.alwaysCalledWithExactly(flagSpy, options); sinon.assert.callCount(maxSpy, 2); diff --git a/test/limit.test.js b/test/limit.test.js index 96543a9865d..4fab090dac6 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -9,16 +9,24 @@ const {MaxLimit, AllowlistLimit, FlagLimit, MaxPeriodicLimit} = require('../lib/ describe('Limit Service', function () { describe('Flag Limit', function () { - it('do nothing if is over limit', async function () { - // NOTE: the behavior of flag limit in "is over limit" usecase is flawed and should not be relied on - // possible solution could be throwing an error to prevent clients from using it? + it('throws if is over limit when disabled', async function () { const config = { disabled: true }; - const limit = new FlagLimit({name: 'flaggy', config, errors}); + const limit = new FlagLimit({name: 'limitFlaggy', config, errors}); - const result = await limit.errorIfIsOverLimit(); - should(result).be.undefined(); + try { + await limit.errorIfIsOverLimit(); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.exist(err.errorType); + should.equal(err.errorType, 'HostLimitError'); + should.exist(err.errorDetails); + should.equal(err.errorDetails.name, 'limitFlaggy'); + should.exist(err.message); + should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.'); + } }); it('throws if would go over limit', async function () { @@ -43,6 +51,78 @@ describe('Limit Service', function () { should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.'); } }); + + it('does not throw if feature is in use when currentCountQuery returns true', async function () { + const config = { + disabled: true, + currentCountQuery: () => true + }; + const limit = new FlagLimit({name: 'flaggy', config, errors}); + + const result = await limit.errorIfIsOverLimit(); + should(result).be.undefined(); + }); + + it('throws if feature is not in use when currentCountQuery returns false', async function () { + const config = { + disabled: true, + currentCountQuery: () => false + }; + const limit = new FlagLimit({name: 'limitFlaggy', config, errors}); + + try { + await limit.errorIfIsOverLimit(); + should.fail(limit, 'Should have errored'); + } catch (err) { + should.exist(err); + should.equal(err.errorType, 'HostLimitError'); + } + }); + + it('calls currentCountQuery with transacting option', async function () { + const currentCountQueryStub = sinon.stub().resolves(true); + const config = { + disabled: true, + currentCountQuery: currentCountQueryStub + }; + const db = { + knex: 'connection' + }; + const limit = new FlagLimit({name: 'flaggy', config, db, errors}); + const transaction = 'transaction'; + + await limit.errorIfIsOverLimit({transacting: transaction}); + + sinon.assert.calledOnce(currentCountQueryStub); + sinon.assert.calledWithExactly(currentCountQueryStub, transaction); + }); + + it('errorIfWouldGoOverLimit behaves the same as errorIfIsOverLimit', async function () { + const config = { + disabled: true, + currentCountQuery: () => true + }; + const limit = new FlagLimit({name: 'flaggy', config, errors}); + + // Should not throw when feature is in use + const result = await limit.errorIfWouldGoOverLimit(); + should(result).be.undefined(); + + // Should throw when feature is not in use + const config2 = { + disabled: true, + currentCountQuery: () => false + }; + const limit2 = new FlagLimit({name: 'limitFlaggy', config: config2, errors}); + + try { + await limit2.errorIfWouldGoOverLimit(); + should.fail(limit2, 'Should have errored'); + } catch (err) { + should.exist(err); + should.equal(err.errorType, 'HostLimitError'); + } + }); }); describe('Max Limit', function () { From 29ce0b3e4c3eb94cc35d47b8d45c8fb96ecbc4a4 Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Wed, 25 Jun 2025 14:35:07 +0400 Subject: [PATCH 206/255] Removed `currentCountQuery` from `limitAnalytics` ref BAE-336 ref BAE-330 This was a working approach, but in order to achieve grandfathering we'll try overwriting the limit at the source in Zuul by adding a limit specific cohort with all site IDs that are currently using the feature and will be affected by the limit, so they can be grandfathered and keep using it. --- lib/config.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/config.js b/lib/config.js index 81d9da5f464..63d5103604a 100644 --- a/lib/config.js +++ b/lib/config.js @@ -57,17 +57,6 @@ module.exports = { formatter: count => `${count / 1000000}MB` }, limitStripeConnect: {}, - limitAnalytics: { - currentCountQuery: async (knex) => { - const key = 'trafficAnalytics'; - const settings = await knex('settings'); - const setting = settings.find(s => s.key === key); - const labSettings = settings.find(s => s.key === 'labs'); - const labsSetting = labSettings && labSettings.value && labSettings.value[key]; - const labsSettingEnabled = labsSetting === true; - - return (setting && setting.value === 'true') || labsSettingEnabled; - } - }, + limitAnalytics: {}, limitActivityPub: {} }; From 8a0afd8d613c032d889c4804eb04127137df22cf Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Thu, 26 Jun 2025 12:46:24 +0400 Subject: [PATCH 207/255] Published new versions - @tryghost/limit-service@1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a03ba6ba1a4..00f2e9d0211 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.2.19", + "version": "1.3.0", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 9a5a0b79f4350529cb7aa37a80f727cb30498e5c Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Thu, 26 Jun 2025 18:07:27 +0400 Subject: [PATCH 208/255] Renamed `limitActivityPub` to `limitSocialWeb` --- README.md | 22 +++++++++++----------- lib/config.js | 2 +- test/LimitService.test.js | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 7b2657823e4..6d857067395 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ const limitService = new LimitService(); // setup limit configuration // currently supported limit keys are: staff, members, customThemes, customIntegrations, uploads, -// limitStripeConnect, limitAnalytics, and limitActivityPub +// limitStripeConnect, limitAnalytics, and limitSocialWeb // all limit configs support custom "error" configuration that is a template string const limits = { // staff and member are "max" type of limits accepting "max" configuration @@ -64,17 +64,17 @@ const limits = { uploads: { // max key is in bytes max: 5000000, - // formatting of the {{ max }} vairable is in MB, e.g: 5MB + // formatting of the {{ max }} variable is in MB, e.g: 5MB error: 'Your plan supports uploads of max size up to {{max}}. Please upgrade to reenable uploading.' }, limitStripeConnect: {}, limitAnalytics: {}, - limitActivityPub: {} + limitSocialWeb: {} }; // This information is needed for the limit service to work with "max periodic" limits // The interval value has to be 'month' as that's the only interval that was needed for -// current usecase +// current use case // The startDate has to be in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601) const subscription = { interval: 'month', @@ -112,7 +112,7 @@ if (limitService.isLimited('staff')) { await limitService.errorIfWouldGoOverLimit('staff', {max: 100}); } -// "max" types of limits have currentCountQuery method reguring a number that is currently in use for the limit +// "max" types of limits have currentCountQuery method requiring a number that is currently in use for the limit // for example it could be 1, 3, 5 or whatever amount of 'staff' is currently in the system const staffCount = await limitService.currentCountQuery('staff'); @@ -158,18 +158,18 @@ db.transaction((transacting) => { ### Types of limits At the moment there are four different types of limits that limit service allows to define. These types are: -1. `flag` - is an "on/off" switch for certain feature. Example usecase: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. It is possible to overwrite the limit by providing a `currentCountQuery` for it. This is useful in cases where we introduce new limits to existing plans and customers have already been using the feature affected by the limit. By providing a `currentCountQuery` that detects if the feature is already in use, we won't disable it. -2. `max` - checks if the maximum amount of the resource has been used up.Example usecase: "disable creating a staff user when maximum of 5 has been reached". To configure this limit add `max: NUMBER` to the configuration. The limits that support max checks are: `members`, and `staff` -3. `maxPeriodic` - it's a variation of `max` type with a difference that the check is done over certain period of time. Example usecase: "disable sending emails when the sent emails count has acceded a limit for last billing period". To enable this limit define `maxPeriodic: NUMBER` in the limit configuration and provide a subscription configuration when initializing the limit service instance. The subscription object comes as a separate parameter and has to contain two properties: `startDate` and `interval`, where `startDate` is a date in ISO 8601 format and period is `'month'` (other values like `'year'` are not supported yet) -4. `allowList` - checks if provided value is defined in configured "allowlist". Example usecase: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. +1. `flag` - is an "on/off" switch for certain feature. Example use case: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. It is possible to overwrite the limit by providing a `currentCountQuery` for it. This is useful in cases where we introduce new limits to existing plans and customers have already been using the feature affected by the limit. By providing a `currentCountQuery` that detects if the feature is already in use, we won't disable it. +2. `max` - checks if the maximum amount of the resource has been used up.Example use case: "disable creating a staff user when maximum of 5 has been reached". To configure this limit add `max: NUMBER` to the configuration. The limits that support max checks are: `members`, and `staff` +3. `maxPeriodic` - it's a variation of `max` type with a difference that the check is done over certain period of time. Example use case: "disable sending emails when the sent emails count has acceded a limit for last billing period". To enable this limit define `maxPeriodic: NUMBER` in the limit configuration and provide a subscription configuration when initializing the limit service instance. The subscription object comes as a separate parameter and has to contain two properties: `startDate` and `interval`, where `startDate` is a date in ISO 8601 format and period is `'month'` (other values like `'year'` are not supported yet) +4. `allowList` - checks if provided value is defined in configured "allowlist". Example use case: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. ### Supported limits -There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`, `uploads`, `limitStripeConnect`, `limitAnalytics`, and `limitActivityPub`. +There's a limited amount of limits that are supported by limit service. The are defined by "key" property name in the "config" module. List of currently supported limit names: `members`, `staff`, `customIntegrations`, `emails`, `customThemes`, `uploads`, `limitStripeConnect`, `limitAnalytics`, and `limitSocialWeb`. All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff`) can have a `max` limit. Only `emails` currently supports the `maxPeriodic` type of limit. ### Frontend usage -In case the limit check is run without direct access to the database you can override `currentCountQuery` functions for each "max" or "maxPeriodic" type of limit. An example usecase would be a frontend client running in a browser. A browser client can check the limit data through HTTP request and then provide that data to the limit service. Example code to do exactly that: +In case the limit check is run without direct access to the database you can override `currentCountQuery` functions for each "max" or "maxPeriodic" type of limit. An example use case would be a frontend client running in a browser. A browser client can check the limit data through HTTP request and then provide that data to the limit service. Example code to do exactly that: ```js const limitService = new LimitService(); diff --git a/lib/config.js b/lib/config.js index 63d5103604a..3b752d8f8a1 100644 --- a/lib/config.js +++ b/lib/config.js @@ -58,5 +58,5 @@ module.exports = { }, limitStripeConnect: {}, limitAnalytics: {}, - limitActivityPub: {} + limitSocialWeb: {} }; diff --git a/test/LimitService.test.js b/test/LimitService.test.js index b33ed1bfd2c..e8e8bec14d8 100644 --- a/test/LimitService.test.js +++ b/test/LimitService.test.js @@ -133,22 +133,22 @@ describe('Limit Service', function () { members: {max: 100}, emails: {disabled: true}, limitStripeConnect: {disabled: true}, - limitActivityPub: {disabled: true} + limitSocialWeb: {disabled: true} }; limitService.loadLimits({limits, errors}); - limitService.limits.should.be.an.Object().with.properties(['staff', 'members', 'emails', 'limitStripeConnect', 'limitActivityPub']); + limitService.limits.should.be.an.Object().with.properties(['staff', 'members', 'emails', 'limitStripeConnect', 'limitSocialWeb']); limitService.limits.staff.should.be.an.instanceOf(MaxLimit); limitService.limits.members.should.be.an.instanceOf(MaxLimit); limitService.limits.emails.should.be.an.instanceOf(FlagLimit); limitService.limits.limitStripeConnect.should.be.an.instanceOf(FlagLimit); - limitService.limits.limitActivityPub.should.be.an.instanceOf(FlagLimit); + limitService.limits.limitSocialWeb.should.be.an.instanceOf(FlagLimit); limitService.isLimited('staff').should.be.true(); limitService.isLimited('members').should.be.true(); limitService.isLimited('emails').should.be.true(); limitService.isLimited('limitStripeConnect').should.be.true(); - limitService.isLimited('limitActivityPub').should.be.true(); + limitService.isLimited('limitSocialWeb').should.be.true(); }); it('can load camel cased limits', function () { @@ -266,7 +266,7 @@ describe('Limit Service', function () { limitStripeConnect: { disabled: true }, - limitActivityPub: { + limitSocialWeb: { disabled: true } }; @@ -313,7 +313,7 @@ describe('Limit Service', function () { disabled: true // No currentCountQuery - will be considered over limit }, - limitActivityPub: { + limitSocialWeb: { disabled: true // No currentCountQuery - will be considered over limit } @@ -514,7 +514,7 @@ describe('Limit Service', function () { limitStripeConnect: { disabled: false // Not disabled, so won't be over limit }, - limitActivityPub: { + limitSocialWeb: { disabled: false // Not disabled, so won't be over limit } }; @@ -537,7 +537,7 @@ describe('Limit Service', function () { // Should return false because no limits are exceeded (await limitService.checkIfAnyOverLimit(options)).should.be.false(); - // We have 4 flag limits now: customIntegrations, limitAnalytics, limitStripeConnect, and limitActivityPub + // We have 4 flag limits now: customIntegrations, limitAnalytics, limitStripeConnect, and limitSocialWeb sinon.assert.callCount(flagSpy, 4); sinon.assert.alwaysCalledWithExactly(flagSpy, options); From 200767c2426e45c8e29affc59059818d7174c96e Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Thu, 26 Jun 2025 18:08:39 +0400 Subject: [PATCH 209/255] Published new versions - @tryghost/limit-service@1.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 00f2e9d0211..20eb0859807 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.3.0", + "version": "1.3.1", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 11eed88d201ae8816c416d453e878f99f9e0fbc4 Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Fri, 27 Jun 2025 20:01:51 +0400 Subject: [PATCH 210/255] Reverted currentCountQuery support for flag limits ref BAE-331 The currentCountQuery functionality for flag limits introduced unexpected behavior that allowed disabled features to remain enabled when already in use. This grandfathering mechanism was intended to prevent breaking existing customer setups when introducing new pricing limits, but it created inconsistencies in limit enforcement. The changes remove the ability for flag limits to check if a feature is already in use and override the disabled state. Flag limits now behave as simple on/off switches - when disabled, errorIfWouldGoOverLimit throws an error, and errorIfIsOverLimit returns without checking anything. This restores the original, predictable behavior where disabled means disabled without exceptions. --- README.md | 2 +- lib/limit.js | 53 +++------------------- test/LimitService.test.js | 12 ++--- test/limit.test.js | 92 +++------------------------------------ 4 files changed, 17 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 6d857067395..3fee6c8a670 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ db.transaction((transacting) => { ### Types of limits At the moment there are four different types of limits that limit service allows to define. These types are: -1. `flag` - is an "on/off" switch for certain feature. Example use case: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. It is possible to overwrite the limit by providing a `currentCountQuery` for it. This is useful in cases where we introduce new limits to existing plans and customers have already been using the feature affected by the limit. By providing a `currentCountQuery` that detects if the feature is already in use, we won't disable it. +1. `flag` - is an "on/off" switch for certain feature. Example use case: "disable all emails". It's identified by a `disabled: true` property in the "limits" configuration. 2. `max` - checks if the maximum amount of the resource has been used up.Example use case: "disable creating a staff user when maximum of 5 has been reached". To configure this limit add `max: NUMBER` to the configuration. The limits that support max checks are: `members`, and `staff` 3. `maxPeriodic` - it's a variation of `max` type with a difference that the check is done over certain period of time. Example use case: "disable sending emails when the sent emails count has acceded a limit for last billing period". To enable this limit define `maxPeriodic: NUMBER` in the limit configuration and provide a subscription configuration when initializing the limit service instance. The subscription object comes as a separate parameter and has to contain two properties: `startDate` and `interval`, where `startDate` is a date in ISO 8601 format and period is `'month'` (other values like `'year'` are not supported yet) 4. `allowList` - checks if provided value is defined in configured "allowlist". Example use case: "disable theme activation if it is not an official theme". To configure this limit define ` allowlist: ['VALUE_1', 'VALUE_2', 'VALUE_N']` property in the "limits" parameter. diff --git a/lib/limit.js b/lib/limit.js index e7fbfdf4dc1..363f39ac203 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -264,7 +264,6 @@ class FlagLimit extends Limit { * @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 {Function} [options.config.currentCountQuery] - query checking the state that would be compared against the limit * @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) */ @@ -274,7 +273,6 @@ class FlagLimit extends Limit { this.disabled = config.disabled; this.fallbackMessage = `Your plan does not support ${userFacingLimitName}. Please upgrade to enable ${userFacingLimitName}.`; - this.currentCountQueryFn = config?.currentCountQuery || null; } generateError() { @@ -290,59 +288,20 @@ class FlagLimit extends Limit { } /** - * @param {Object} [options] - * @param {Object} [options.transacting] Transaction to run the count query on - * @returns {Promise} - returns the current count of items that would be compared against the limit + * Flag limits are on/off so using a feature is always over the limit */ - async currentCountQuery(options = {}) { - if (!this.currentCountQueryFn || typeof this.currentCountQueryFn !== 'function') { - return false; - } - - return await this.currentCountQueryFn(options.transacting ?? this.db?.knex); - } - - // As Flag limits are on/off, we won't check against max values. - // `errorIfWouldGoOverLimit` and `errorIfIsOverLimit` end up doing the same thing. - async _isOrWouldOverLimitError(options = {}) { - if (!this.disabled) { - return; - } - - // If no currentCountQuery is provided, throw error when disabled - if (!this.currentCountQueryFn || typeof this.currentCountQueryFn !== 'function') { + async errorIfWouldGoOverLimit() { + if (this.disabled) { throw this.generateError(); } - - // If currentCountQuery is provided, check if feature is in use - const featureInUse = await this.currentCountQuery(options); - - // Only throw error if feature is NOT in use (allowing grandfathering) - if (!featureInUse) { - throw this.generateError(); - } - } - - /** - * Flag limits are usually on/off so using a feature is always over the limit, - * unless the limit has a currentCountQuery function provided to check if the - * feature is in use. This is a use case for when we introduce a new limit and - * customers have already been using this feature. We don't want to take it - * away from them. - */ - async errorIfWouldGoOverLimit(options = {}) { - await this._isOrWouldOverLimitError(options); } /** * Flag limits are on/off. They don't necessarily mean the limit wasn't possible to reach - * Exception: the limit has a currentCountQuery function provided to check if the - * feature is in use. This is a use case for when we introduce a new limit and - * customers have already been using this feature. We don't want to take it - * away from them. + * NOTE: this method should not be relied on as it's impossible to check the limit was surpassed! */ - async errorIfIsOverLimit(options = {}) { - await this._isOrWouldOverLimitError(options); + async errorIfIsOverLimit() { + return; } } diff --git a/test/LimitService.test.js b/test/LimitService.test.js index e8e8bec14d8..311ac713083 100644 --- a/test/LimitService.test.js +++ b/test/LimitService.test.js @@ -281,7 +281,7 @@ describe('Limit Service', function () { (await limitService.checkIfAnyOverLimit()).should.be.true(); }); - it('Confirms when a flag limit without currentCountQuery is disabled', async function () { + it('Does not check flag limits when checking if any are over limit', async function () { const limitService = new LimitService(); let limits = { @@ -303,19 +303,15 @@ describe('Limit Service', function () { // }, customIntegrations: { disabled: true - // No currentCountQuery - will be considered over limit }, limitAnalytics: { - disabled: true, - currentCountQuery: () => true // Feature is in use, so limit won't be exceeded (grandfathered) + disabled: true }, limitStripeConnect: { disabled: true - // No currentCountQuery - will be considered over limit }, limitSocialWeb: { disabled: true - // No currentCountQuery - will be considered over limit } }; @@ -326,8 +322,8 @@ describe('Limit Service', function () { limitService.loadLimits({limits, errors, subscription}); - // Should return true because customIntegrations is disabled without currentCountQuery - (await limitService.checkIfAnyOverLimit()).should.be.true(); + // Should return false because flag limits' errorIfIsOverLimit does not throw + (await limitService.checkIfAnyOverLimit()).should.be.false(); }); it('Returns nothing if limit is not configured', async function () { diff --git a/test/limit.test.js b/test/limit.test.js index 4fab090dac6..038f5a72f97 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -9,24 +9,16 @@ const {MaxLimit, AllowlistLimit, FlagLimit, MaxPeriodicLimit} = require('../lib/ describe('Limit Service', function () { describe('Flag Limit', function () { - it('throws if is over limit when disabled', async function () { + it('do nothing if is over limit', async function () { + // NOTE: the behavior of flag limit in "is over limit" use case is flawed and should not be relied on + // possible solution could be throwing an error to prevent clients from using it? const config = { disabled: true }; - const limit = new FlagLimit({name: 'limitFlaggy', config, errors}); + const limit = new FlagLimit({name: 'flaggy', config, errors}); - try { - await limit.errorIfIsOverLimit(); - should.fail(limit, 'Should have errored'); - } catch (err) { - should.exist(err); - should.exist(err.errorType); - should.equal(err.errorType, 'HostLimitError'); - should.exist(err.errorDetails); - should.equal(err.errorDetails.name, 'limitFlaggy'); - should.exist(err.message); - should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.'); - } + const result = await limit.errorIfIsOverLimit(); + should(result).be.undefined(); }); it('throws if would go over limit', async function () { @@ -51,78 +43,6 @@ describe('Limit Service', function () { should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.'); } }); - - it('does not throw if feature is in use when currentCountQuery returns true', async function () { - const config = { - disabled: true, - currentCountQuery: () => true - }; - const limit = new FlagLimit({name: 'flaggy', config, errors}); - - const result = await limit.errorIfIsOverLimit(); - should(result).be.undefined(); - }); - - it('throws if feature is not in use when currentCountQuery returns false', async function () { - const config = { - disabled: true, - currentCountQuery: () => false - }; - const limit = new FlagLimit({name: 'limitFlaggy', config, errors}); - - try { - await limit.errorIfIsOverLimit(); - should.fail(limit, 'Should have errored'); - } catch (err) { - should.exist(err); - should.equal(err.errorType, 'HostLimitError'); - } - }); - - it('calls currentCountQuery with transacting option', async function () { - const currentCountQueryStub = sinon.stub().resolves(true); - const config = { - disabled: true, - currentCountQuery: currentCountQueryStub - }; - const db = { - knex: 'connection' - }; - const limit = new FlagLimit({name: 'flaggy', config, db, errors}); - const transaction = 'transaction'; - - await limit.errorIfIsOverLimit({transacting: transaction}); - - sinon.assert.calledOnce(currentCountQueryStub); - sinon.assert.calledWithExactly(currentCountQueryStub, transaction); - }); - - it('errorIfWouldGoOverLimit behaves the same as errorIfIsOverLimit', async function () { - const config = { - disabled: true, - currentCountQuery: () => true - }; - const limit = new FlagLimit({name: 'flaggy', config, errors}); - - // Should not throw when feature is in use - const result = await limit.errorIfWouldGoOverLimit(); - should(result).be.undefined(); - - // Should throw when feature is not in use - const config2 = { - disabled: true, - currentCountQuery: () => false - }; - const limit2 = new FlagLimit({name: 'limitFlaggy', config: config2, errors}); - - try { - await limit2.errorIfWouldGoOverLimit(); - should.fail(limit2, 'Should have errored'); - } catch (err) { - should.exist(err); - should.equal(err.errorType, 'HostLimitError'); - } - }); }); describe('Max Limit', function () { From f0e9c2ed2f0e79ae2f979559d690637f5fb96fbb Mon Sep 17 00:00:00 2001 From: Aileen Booker Date: Fri, 27 Jun 2025 20:04:46 +0400 Subject: [PATCH 211/255] Published new versions - @tryghost/content-api@1.11.28 - @tryghost/helpers-gatsby@2.0.28 - @tryghost/helpers@1.1.97 - @tryghost/limit-service@1.3.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20eb0859807..97d6612457d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.3.1", + "version": "1.3.2", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 3ec7c956bd0de772be449ee836cb125eebee559e Mon Sep 17 00:00:00 2001 From: Sag Date: Tue, 8 Jul 2025 19:11:59 +0200 Subject: [PATCH 212/255] Added .isDisabled() method to the Limit Service (#550) ref https://linear.app/ghost/issue/PROD-2172 - Limits such as `FlagLimit` are calculated solely based on their `disabled` boolean field - The Limit Service only exposes async methods such as `checkWouldGoOverLimit`, which forces downstream methods in e.g. Ghost to be async too - The new `.isDisabled()` sync method returns true/false based on the value of the `disabled` field, or throws an error if the limit does not support a `disabled` field --- README.md | 18 +++++++++++-- lib/LimitService.js | 21 +++++++++++++++ lib/limit.js | 8 ++++++ test/LimitService.test.js | 55 +++++++++++++++++++++++++++++++++++++++ test/limit.test.js | 32 ++++++++++++++++++++++- 5 files changed, 131 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3fee6c8a670..1a5cfd1cc01 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,12 @@ const limits = { error: 'Your plan supports uploads of max size up to {{max}}. Please upgrade to reenable uploading.' }, limitStripeConnect: {}, - limitAnalytics: {}, - limitSocialWeb: {} + limitAnalytics: { + disabled: false + }, + limitSocialWeb: { + disabled: true + } }; // This information is needed for the limit service to work with "max periodic" limits @@ -134,6 +138,16 @@ if (limitService.isLimited('uploads')) { await limitService.errorIfIsOverLimit('uploads', {currentCount: frame.file.size}); } +// Limits expose an async `checkWouldGoOverLimit` method, which can be used to check whether a limit has been reached, but not throw an error: +if (await limitService.checkWouldGoOverLimit('members')) { + console.log('Members limit has been reached!'); +} + +// Flag limits additionally expose a `isDisabled` sync check, which can be used instead of the async `checkWouldGoOverLimit`: +if (limitService.isDisabled('limitSocialWeb')) { + console.log('Social web is disabled by config!')); +} + // check if any of the limits are acceding if (limitService.checkIfAnyOverLimit()) { console.log('One of the limits has acceded!'); diff --git a/lib/LimitService.js b/lib/LimitService.js index c4ece8ae6d5..616cb97cb08 100644 --- a/lib/LimitService.js +++ b/lib/LimitService.js @@ -69,6 +69,27 @@ class LimitService { return !!this.limits[camelCase(limitName)]; } + /** + * Check if a limit is disabled, applicable only to limits that support the disabled flag (e.g. FlagLimit) + * @returns {boolean|undefined} undefined if limit is not configured + * @throws {IncorrectUsageError} if limit does not support disabled flag + */ + isDisabled(limitName) { + if (!this.isLimited(limitName)) { + return; + } + + const limit = this.limits[camelCase(limitName)]; + + if (typeof limit.isDisabled !== 'function') { + throw new IncorrectUsageError({ + message: `Limit ${limitName} does not support .isDisabled()` + }); + } + + return limit.isDisabled(); + } + /** * * @param {String} limitName - name of the configured limit diff --git a/lib/limit.js b/lib/limit.js index 363f39ac203..d35b8563138 100644 --- a/lib/limit.js +++ b/lib/limit.js @@ -303,6 +303,14 @@ class FlagLimit extends Limit { async errorIfIsOverLimit() { return; } + + /** + * Checks whether the Flag limit is disabled or not + * @returns boolean + */ + isDisabled() { + return !!this.disabled; + } } class AllowlistLimit extends Limit { diff --git a/test/LimitService.test.js b/test/LimitService.test.js index 311ac713083..27e9e6b8b0f 100644 --- a/test/LimitService.test.js +++ b/test/LimitService.test.js @@ -2,6 +2,7 @@ // const testUtils = require('./utils'); require('./utils'); const should = require('should'); +const assert = require('node:assert').strict; const LimitService = require('../lib/LimitService'); const {MaxLimit, MaxPeriodicLimit, FlagLimit} = require('../lib/limit'); const sinon = require('sinon'); @@ -544,4 +545,58 @@ describe('Limit Service', function () { sinon.assert.alwaysCalledWithExactly(maxPeriodSpy, options); }); }); + + describe('isDisabled', function () { + it('returns undefined if limit is not configured', function () { + const limitService = new LimitService(); + + assert.equal(limitService.isDisabled('test'), undefined); + }); + + it('throws if the limit does not implement .isDisabled()', function () { + const limitService = new LimitService(); + + let limits = { + staff: { + max: 2, + currentCountQuery: () => 1 + } + }; + + limitService.loadLimits({limits, errors}); + + try { + limitService.isDisabled('staff'); + assert.fail('Should have thrown an error'); + } catch (err) { + assert.equal(err.message, `Limit staff does not support .isDisabled()`); + } + }); + + it('returns true if the limit is disabled', function () { + const limitService = new LimitService(); + + let limits = { + limitSocialWeb: { + disabled: true + } + }; + + limitService.loadLimits({limits, errors}); + assert.equal(limitService.isDisabled('limitSocialWeb'), true); + }); + + it('returns false if the limit is not disabled', function () { + const limitService = new LimitService(); + + let limits = { + limitSocialWeb: { + disabled: false + } + }; + + limitService.loadLimits({limits, errors}); + assert.equal(limitService.isDisabled('limitSocialWeb'), false); + }); + }); }); diff --git a/test/limit.test.js b/test/limit.test.js index 038f5a72f97..ea1c41003ba 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -3,11 +3,12 @@ require('./utils'); const should = require('should'); const sinon = require('sinon'); +const assert = require('node:assert').strict; const errors = require('./fixtures/errors'); const {MaxLimit, AllowlistLimit, FlagLimit, MaxPeriodicLimit} = require('../lib/limit'); -describe('Limit Service', function () { +describe('Limit', function () { describe('Flag Limit', function () { it('do nothing if is over limit', async function () { // NOTE: the behavior of flag limit in "is over limit" use case is flawed and should not be relied on @@ -43,6 +44,35 @@ describe('Limit Service', function () { should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.'); } }); + + describe('isDisabled', function () { + it('returns true if limit is disabled', function () { + const config = { + disabled: true + }; + const limit = new FlagLimit({name: 'flaggy', config, errors}); + + assert.equal(limit.isDisabled(), true); + }); + + it('returns false if limit is disabled', function () { + const config = { + disabled: false + }; + const limit = new FlagLimit({name: 'flaggy', config, errors}); + + assert.equal(limit.isDisabled(), false); + }); + + it('returns false if limit is not defined', function () { + const config = { + disabled: undefined + }; + const limit = new FlagLimit({name: 'flaggy', config, errors}); + + assert.equal(limit.isDisabled(), false); + }); + }); }); describe('Max Limit', function () { From 8d96332ce8e526b785234367464f6d1dc3e633cd Mon Sep 17 00:00:00 2001 From: Sag Date: Tue, 8 Jul 2025 19:19:24 +0200 Subject: [PATCH 213/255] Published new versions (#560) - @tryghost/color-utils@0.2.9 - @tryghost/limit-service@1.4.0 - @tryghost/social-urls@0.1.53 - @tryghost/timezone-data@0.4.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97d6612457d..b0ba3c31a2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.3.2", + "version": "1.4.0", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From b7990b183e24eb65e47679d899964c8eece7fd4c Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Tue, 22 Jul 2025 13:35:49 +0100 Subject: [PATCH 214/255] Published new versions - @tryghost/admin-api@1.14.0 - @tryghost/color-utils@0.2.10 - @tryghost/content-api@1.12.0 - @tryghost/limit-service@1.4.1 - @tryghost/social-urls@0.1.54 - @tryghost/timezone-data@0.4.12 - @tryghost/url-utils@4.4.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0ba3c31a2a..b49b04c6ef3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.4.0", + "version": "1.4.1", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 842098ed211557406d970ee2a756363dfd222480 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:52:40 +0000 Subject: [PATCH 215/255] Update dependency mocha to v11.7.2 (#547) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b49b04c6ef3..71e903015e7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "c8": "10.1.3", - "mocha": "11.2.2", + "mocha": "11.7.2", "should": "13.2.3", "sinon": "21.0.0" }, From 33b128bc63308c2096426dea27bd98dd37d0834d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:56:43 +0000 Subject: [PATCH 216/255] Update dependency mocha to v11.7.3 (#628) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71e903015e7..32a4dd9917e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "c8": "10.1.3", - "mocha": "11.7.2", + "mocha": "11.7.3", "should": "13.2.3", "sinon": "21.0.0" }, From 023bfb66163e0035c65a33b94c544c6a46a53449 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 02:40:32 +0000 Subject: [PATCH 217/255] Update dependency mocha to v11.7.4 (#631) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 32a4dd9917e..bbe15d86740 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "c8": "10.1.3", - "mocha": "11.7.3", + "mocha": "11.7.4", "should": "13.2.3", "sinon": "21.0.0" }, From 9ba10e693f7f24621b611ddfd923dd377462d53c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 18:44:55 +0000 Subject: [PATCH 218/255] Update dependency mocha to v11.7.5 (#672) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bbe15d86740..62f35487761 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "c8": "10.1.3", - "mocha": "11.7.4", + "mocha": "11.7.5", "should": "13.2.3", "sinon": "21.0.0" }, From 43756cceb4e91396d3743607ff933c0c7d7c07e2 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 19 Nov 2025 14:42:33 +0700 Subject: [PATCH 219/255] Published new versions - @tryghost/adapter-base-cache@0.1.18 - @tryghost/admin-api-schema@4.5.11 - @tryghost/admin-api@1.14.1 - @tryghost/color-utils@0.2.11 - @tryghost/config-url-helpers@1.0.18 - @tryghost/content-api@1.12.1 - @tryghost/custom-fonts@1.0.3 - @tryghost/helpers-gatsby@2.0.29 - @tryghost/helpers@1.1.98 - @tryghost/html-to-plaintext@1.0.5 - @tryghost/image-transform@1.4.7 - @tryghost/limit-service@1.4.2 - @tryghost/referrer-parser@0.1.9 - @tryghost/schema-org@0.1.46 - @tryghost/social-urls@0.1.55 - @tryghost/string@0.2.18 - @tryghost/timezone-data@0.4.13 - @tryghost/url-utils@4.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 62f35487761..cc83312c3e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.4.1", + "version": "1.4.2", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From d5b1fa335cbc0f644e516e30c5ca0806035dc9ed Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Wed, 19 Nov 2025 18:48:34 +0530 Subject: [PATCH 220/255] Published new versions - @tryghost/adapter-base-cache@0.1.19 - @tryghost/admin-api-schema@4.5.12 - @tryghost/admin-api@1.14.2 - @tryghost/color-utils@0.2.12 - @tryghost/config-url-helpers@1.0.19 - @tryghost/content-api@1.12.2 - @tryghost/custom-fonts@1.0.4 - @tryghost/helpers-gatsby@2.0.30 - @tryghost/helpers@1.1.99 - @tryghost/html-to-plaintext@1.0.6 - @tryghost/image-transform@1.4.8 - @tryghost/limit-service@1.4.3 - @tryghost/referrer-parser@0.1.10 - @tryghost/schema-org@0.1.47 - @tryghost/social-urls@0.1.56 - @tryghost/string@0.2.19 - @tryghost/timezone-data@0.4.14 - @tryghost/url-utils@5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc83312c3e9..537220423a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.4.2", + "version": "1.4.3", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 49ceb29445363f2292ee873ac8465c3aa05845d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:54:53 +0000 Subject: [PATCH 221/255] Pin dependency @tryghost/errors to 1.3.8 (#706) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 537220423a4..e5db1ec5335 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "sinon": "21.0.0" }, "dependencies": { - "@tryghost/errors": "^1.2.26", + "@tryghost/errors": "1.3.8", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 76af78ba6263cdf270581bcf6fb3855fe083ae2d Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Wed, 17 Dec 2025 15:35:23 +0530 Subject: [PATCH 222/255] Published new versions - @tryghost/admin-api-schema@4.5.13 - @tryghost/admin-api@1.14.3 - @tryghost/helpers-gatsby@2.0.31 - @tryghost/image-transform@1.4.9 - @tryghost/limit-service@1.4.4 - @tryghost/referrer-parser@0.1.11 - @tryghost/string@0.2.20 - @tryghost/url-utils@5.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5db1ec5335..ec3db072266 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.4.3", + "version": "1.4.4", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From f0049dde9a9c36755fde54af1716fe5271dbfca0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:34:11 +0000 Subject: [PATCH 223/255] Update dependency sinon to v21.0.1 (#729) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec3db072266..b15a80e133f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "10.1.3", "mocha": "11.7.5", "should": "13.2.3", - "sinon": "21.0.0" + "sinon": "21.0.1" }, "dependencies": { "@tryghost/errors": "1.3.8", From 4ef7d5cc77d1ca8175375a088eb7fbf82e6ccc30 Mon Sep 17 00:00:00 2001 From: Rob Lester Date: Mon, 12 Jan 2026 12:54:02 +0000 Subject: [PATCH 224/255] Published new versions - @tryghost/adapter-base-cache@0.1.20 - @tryghost/admin-api-schema@4.6.1 - @tryghost/admin-api@1.14.4 - @tryghost/color-utils@0.2.13 - @tryghost/config-url-helpers@1.0.20 - @tryghost/content-api@1.12.3 - @tryghost/custom-fonts@1.0.5 - @tryghost/helpers-gatsby@2.0.32 - @tryghost/helpers@1.1.100 - @tryghost/image-transform@1.4.10 - @tryghost/limit-service@1.4.5 - @tryghost/referrer-parser@0.1.12 - @tryghost/schema-org@0.1.48 - @tryghost/social-urls@0.1.57 - @tryghost/string@0.2.21 - @tryghost/timezone-data@0.4.15 - @tryghost/url-utils@5.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b15a80e133f..1df42de087a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.4.4", + "version": "1.4.5", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 0daa24431b43306d36de892805dc073fcac0291f Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Wed, 21 Jan 2026 14:59:12 +0000 Subject: [PATCH 225/255] 2026 Co-authored-by: Hannah Wolfe --- LICENSE | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 37c0d47d6f9..efad547e8fc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2013-2025 Ghost Foundation +Copyright (c) 2013-2026 Ghost Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1a5cfd1cc01..7b3abea67bf 100644 --- a/README.md +++ b/README.md @@ -237,4 +237,4 @@ Follow the instructions for the top-level repo. # Copyright & License -Copyright (c) 2013-2025 Ghost Foundation - Released under the [MIT license](LICENSE). +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). From 0a48b8519f0d6239595ac6e6c5b15c093e1ff440 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:54:13 +0000 Subject: [PATCH 226/255] Update dependency @tryghost/errors to v1.3.9 (#766) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1df42de087a..4cd93014905 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/errors": "1.3.8", + "@tryghost/errors": "1.3.9", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 0ffb224b98a5581f209166d25e1639fcc833a450 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:49:15 +0000 Subject: [PATCH 227/255] Update dependency @tryghost/errors to v1.3.10 (#772) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4cd93014905..f72a00d9cbe 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/errors": "1.3.9", + "@tryghost/errors": "1.3.10", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 4cc837e2c3c101f62a31246d26608025907843a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 02:36:50 +0000 Subject: [PATCH 228/255] Update dependency @tryghost/errors to v1.3.13 (#773) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f72a00d9cbe..6be21bbb30e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/errors": "1.3.10", + "@tryghost/errors": "1.3.13", "lodash": "^4.17.21", "luxon": "^1.26.0" } From 29e8bb4db12910ad1cae4c3fc8670aa0fbf63926 Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Tue, 24 Feb 2026 23:35:04 +0530 Subject: [PATCH 229/255] Enforce test coverage thresholds across all packages (#776) ref https://linear.app/ghost/issue/HKG-1626/clean-up-sdk-repository - Add --check-coverage to all packages - Enforce c8 default 90% line coverage threshold across all packages so CI will fail if coverage drops. For color-utils, also added c8 as a devDependency. For referrer-parser (Vitest), added coverage thresholds and enabled --coverage in the test script. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6be21bbb30e..2d2d4404d4a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "exports": "./index.js", "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test": "NODE_ENV=testing c8 --all --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", "lint": "eslint . --ext .js --cache", "posttest": "yarn lint" }, From d6438697fb37489789cfe241b0bbce47c8ecbef7 Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Wed, 25 Feb 2026 00:43:17 +0530 Subject: [PATCH 230/255] Enforce 100% line coverage threshold across all packages (#777) ref https://linear.app/ghost/issue/HKG-1626/ - Set --lines 100 coverage threshold across all packages - Added tests --- package.json | 2 +- test/LimitService.test.js | 27 ++++++++ test/config.test.js | 68 +++++++++++++++++++ test/date-utils.test.js | 10 +++ test/limit.test.js | 137 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 test/config.test.js diff --git a/package.json b/package.json index 2d2d4404d4a..9dd154d73a7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "exports": "./index.js", "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing c8 --all --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test": "NODE_ENV=testing c8 --reporter text --reporter cobertura mocha './test/**/*.test.js'", "lint": "eslint . --ext .js --cache", "posttest": "yarn lint" }, diff --git a/test/LimitService.test.js b/test/LimitService.test.js index 27e9e6b8b0f..8d1098f8a17 100644 --- a/test/LimitService.test.js +++ b/test/LimitService.test.js @@ -10,6 +10,11 @@ const sinon = require('sinon'); const errors = require('./fixtures/errors'); describe('Limit Service', function () { + it('is exported via the package index', function () { + const LimitServiceFromIndex = require('../index'); + assert.equal(LimitServiceFromIndex, LimitService); + }); + describe('Lodash Template', function () { it('Does not get clobbered by this lib', function () { require('../lib/limit'); @@ -364,6 +369,28 @@ describe('Limit Service', function () { }); }); + describe('checkWouldGoOverLimit', function () { + it('rethrows non-HostLimitError from errorIfWouldGoOverLimit', async function () { + const limitService = new LimitService(); + + let limits = { + customThemes: { + allowlist: ['casper', 'dawn', 'lyra'] + } + }; + + limitService.loadLimits({limits, errors}); + + try { + await limitService.checkWouldGoOverLimit('customThemes', {}); + assert.fail('Should have thrown'); + } catch (err) { + assert.equal(err.errorType, 'IncorrectUsageError'); + assert.match(err.message, /allowlist limit without a value/); + } + }); + }); + describe('Metadata', function () { afterEach(function () { sinon.restore(); diff --git a/test/config.test.js b/test/config.test.js new file mode 100644 index 00000000000..43db1119710 --- /dev/null +++ b/test/config.test.js @@ -0,0 +1,68 @@ +require('./utils'); +const assert = require('node:assert').strict; +const sinon = require('sinon'); +const config = require('../lib/config'); + +describe('Config', function () { + afterEach(function () { + sinon.restore(); + }); + + function createMockKnex(options = {}) { + const chain = { + count: sinon.stub().returnsThis(), + sum: sinon.stub().returnsThis(), + where: sinon.stub().returnsThis(), + first: sinon.stub().resolves(options.firstResult || {count: 0}), + select: sinon.stub().returnsThis(), + leftJoin: sinon.stub().returnsThis(), + whereNot: sinon.stub().returnsThis(), + andWhereNot: sinon.stub().returnsThis(), + union: sinon.stub().resolves(options.unionResult || []) + }; + return sinon.stub().returns(chain); + } + + describe('members', function () { + it('queries the members table and returns count', async function () { + const knex = createMockKnex({firstResult: {count: 42}}); + const result = await config.members.currentCountQuery(knex); + + assert.equal(result, 42); + sinon.assert.calledWith(knex, 'members'); + }); + }); + + describe('newsletters', function () { + it('queries active newsletters and returns count', async function () { + const knex = createMockKnex({firstResult: {count: 7}}); + const result = await config.newsletters.currentCountQuery(knex); + + assert.equal(result, 7); + sinon.assert.calledWith(knex, 'newsletters'); + }); + }); + + describe('emails', function () { + it('queries emails since start date and returns sum', async function () { + const knex = createMockKnex({firstResult: {count: 500}}); + const startDate = '2021-01-01T00:00:00Z'; + const result = await config.emails.currentCountQuery(knex, startDate); + + assert.equal(result, 500); + sinon.assert.calledWith(knex, 'emails'); + }); + }); + + describe('staff', function () { + it('queries users with roles and invites and returns count', async function () { + const mockResults = [{id: 1}, {id: 2}, {id: 3}]; + const knex = createMockKnex({unionResult: mockResults}); + const result = await config.staff.currentCountQuery(knex); + + assert.equal(result, 3); + sinon.assert.calledWith(knex, 'users'); + sinon.assert.calledWith(knex, 'invites'); + }); + }); +}); diff --git a/test/date-utils.test.js b/test/date-utils.test.js index e0585e6b0e6..1a347fef102 100644 --- a/test/date-utils.test.js +++ b/test/date-utils.test.js @@ -4,6 +4,7 @@ require('./utils'); const {DateTime} = require('luxon'); const sinon = require('sinon'); +const assert = require('node:assert').strict; const {lastPeriodStart} = require('../lib/date-utils'); describe('Date Utils', function () { @@ -72,5 +73,14 @@ describe('Date Utils', function () { lastPeriodStartDate.should.equal('2021-02-28T01:59:42.000Z'); }); + + it('throws IncorrectUsageError for unsupported interval', function () { + assert.throws(() => { + lastPeriodStart('2021-01-01T00:00:00Z', 'week'); + }, (err) => { + assert.equal(err.message, 'Invalid interval specified. Only "month" value is accepted.'); + return true; + }); + }); }); }); diff --git a/test/limit.test.js b/test/limit.test.js index ea1c41003ba..6080debcfef 100644 --- a/test/limit.test.js +++ b/test/limit.test.js @@ -73,6 +73,22 @@ describe('Limit', function () { assert.equal(limit.isDisabled(), false); }); }); + + it('uses custom error message when error is provided', async function () { + const config = { + disabled: true, + error: 'Custom flag limit error message' + }; + const limit = new FlagLimit({name: 'limitFlaggy', config, errors}); + + try { + await limit.errorIfWouldGoOverLimit(); + should.fail('Should have errored'); + } catch (err) { + assert.equal(err.errorType, 'HostLimitError'); + assert.equal(err.message, 'Custom flag limit error message'); + } + }); }); describe('Max Limit', function () { @@ -381,6 +397,45 @@ describe('Limit', function () { sinon.assert.alwaysCalledWithExactly(config.currentCountQuery, transaction); }); }); + + describe('generateError', function () { + it('includes help link when helpLink is provided', function () { + const helpPreservingErrors = { + IncorrectUsageError: errors.IncorrectUsageError, + HostLimitError: class extends errors.HostLimitError { + constructor(options) { + super(options); + this.help = options.help; + } + } + }; + const config = { + max: 5, + currentCountQuery: () => {}, + error: 'Over the limit of {{max}}' + }; + const limit = new MaxLimit({name: 'maxy', config, helpLink: 'https://example.com/help', errors: helpPreservingErrors}); + const error = limit.generateError(10); + + assert.equal(error.errorDetails.name, 'maxy'); + assert.equal(error.help, 'https://example.com/help'); + }); + + it('falls back to default message when error template throws', function () { + const config = { + max: 5, + currentCountQuery: () => {}, + error: 'Limit reached', + formatter: () => { + throw new Error('formatter failed'); + } + }; + const limit = new MaxLimit({name: 'maxy', config, errors}); + const error = limit.generateError(10); + + assert.equal(error.message, 'This action would exceed the maxy limit on your current plan.'); + }); + }); }); describe('Periodic Max Limit', function () { @@ -671,6 +726,22 @@ describe('Limit', function () { sinon.assert.alwaysCalledWith(config.currentCountQuery, transaction); }); }); + + describe('generateError', function () { + it('falls back to default message when error template throws', function () { + const config = { + maxPeriodic: 100, + currentCountQuery: () => {}, + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + error: '{{max.foo.bar}}' + }; + const limit = new MaxPeriodicLimit({name: 'mailguard', config, errors}); + const error = limit.generateError(50); + + assert.equal(error.message, 'This action would exceed the mailguard limit on your current plan.'); + }); + }); }); describe('Allowlist limit', function () { @@ -704,5 +775,71 @@ describe('Limit', function () { error.errorType.should.equal('HostLimitError'); } }); + + it('uses custom error message in generateError when error is provided', async function () { + const limit = new AllowlistLimit({name: 'test', config: { + allowlist: ['test', 'ok'], + error: 'Custom allowlist error' + }, errors}); + + try { + await limit.errorIfIsOverLimit({value: 'unknown value'}); + should.fail('Should have failed'); + } catch (error) { + assert.equal(error.errorType, 'HostLimitError'); + assert.equal(error.message, 'Custom allowlist error'); + } + }); + + describe('errorIfWouldGoOverLimit', function () { + it('passes for values in the allowlist', async function () { + const limit = new AllowlistLimit({name: 'test', config: { + allowlist: ['test', 'ok'] + }, errors}); + + await limit.errorIfWouldGoOverLimit({value: 'test'}); + }); + + it('throws for values not in the allowlist', async function () { + const limit = new AllowlistLimit({name: 'test', config: { + allowlist: ['test', 'ok'] + }, errors}); + + try { + await limit.errorIfWouldGoOverLimit({value: 'unknown'}); + should.fail('Should have failed'); + } catch (error) { + assert.equal(error.errorType, 'HostLimitError'); + } + }); + + it('throws IncorrectUsageError when metadata is missing', async function () { + const limit = new AllowlistLimit({name: 'test', config: { + allowlist: ['test', 'ok'] + }, errors}); + + try { + await limit.errorIfWouldGoOverLimit(); + should.fail('Should have failed'); + } catch (error) { + assert.equal(error.errorType, 'IncorrectUsageError'); + assert.match(error.message, /allowlist limit without a value/); + } + }); + + it('throws IncorrectUsageError when metadata.value is missing', async function () { + const limit = new AllowlistLimit({name: 'test', config: { + allowlist: ['test', 'ok'] + }, errors}); + + try { + await limit.errorIfWouldGoOverLimit({}); + should.fail('Should have failed'); + } catch (error) { + assert.equal(error.errorType, 'IncorrectUsageError'); + assert.match(error.message, /allowlist limit without a value/); + } + }); + }); }); }); From 943d54c3723bdf6188cddeef2b4edd94aa46211c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:12:33 +0530 Subject: [PATCH 231/255] Pin dependencies (#707) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9dd154d73a7..6e4d452dc58 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@tryghost/errors": "1.3.13", - "lodash": "^4.17.21", - "luxon": "^1.26.0" + "lodash": "4.17.23", + "luxon": "1.28.1" } } From 6a35d8e9033e647d71f287ece96a362626689d7c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:32:03 +0530 Subject: [PATCH 232/255] Update dependency luxon to v3 (#439) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e4d452dc58..2827835fd51 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,6 @@ "dependencies": { "@tryghost/errors": "1.3.13", "lodash": "4.17.23", - "luxon": "1.28.1" + "luxon": "3.7.2" } } From b682e13abc10b7ded4849ffe9f4939c0d362a055 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:43:44 +0530 Subject: [PATCH 233/255] Update dependency @tryghost/errors to v2 (#774) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2827835fd51..ee1bda35ac5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/errors": "1.3.13", + "@tryghost/errors": "2.2.1", "lodash": "4.17.23", "luxon": "3.7.2" } From 287b2103ae2780b8fa83acd1e5799f662acbdcbd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:54:36 +0000 Subject: [PATCH 234/255] Update dependency c8 to v11 (#789) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee1bda35ac5..97a6a4be7eb 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "access": "public" }, "devDependencies": { - "c8": "10.1.3", + "c8": "11.0.0", "mocha": "11.7.5", "should": "13.2.3", "sinon": "21.0.1" From 90fe96da9669ab69b620550d94cdd167e14eaaac Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Thu, 26 Feb 2026 17:00:08 +0530 Subject: [PATCH 235/255] Published new versions - @tryghost/adapter-base-cache@0.1.21 - @tryghost/admin-api@1.14.5 - @tryghost/admin-api-schema@4.7.0 - @tryghost/color-utils@0.2.14 - @tryghost/config-url-helpers@1.0.21 - @tryghost/content-api@1.12.4 - @tryghost/custom-fonts@1.0.6 - @tryghost/helpers@1.1.101 - @tryghost/helpers-gatsby@2.1.0 - @tryghost/html-to-plaintext@1.0.7 - @tryghost/image-transform@1.4.11 - @tryghost/limit-service@1.5.0 - @tryghost/members-csv@2.0.4 - @tryghost/referrer-parser@0.1.13 - @tryghost/schema-org@0.1.49 - @tryghost/social-urls@0.1.58 - @tryghost/string@0.3.0 - @tryghost/timezone-data@0.4.16 - @tryghost/url-utils@5.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97a6a4be7eb..b943e5f4cc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.4.5", + "version": "1.5.0", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 7a5b7d0e67c3b8ebd56629656ef8c61981f35763 Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Thu, 26 Feb 2026 18:01:48 +0530 Subject: [PATCH 236/255] Published new versions - @tryghost/adapter-base-cache@0.1.22 - @tryghost/admin-api@1.14.6 - @tryghost/admin-api-schema@4.7.1 - @tryghost/color-utils@0.2.15 - @tryghost/config-url-helpers@1.0.22 - @tryghost/content-api@1.12.5 - @tryghost/custom-fonts@1.0.7 - @tryghost/helpers@1.1.102 - @tryghost/helpers-gatsby@2.1.1 - @tryghost/html-to-plaintext@1.0.8 - @tryghost/image-transform@1.4.12 - @tryghost/limit-service@1.5.1 - @tryghost/members-csv@2.0.5 - @tryghost/referrer-parser@0.1.14 - @tryghost/schema-org@0.1.50 - @tryghost/social-urls@0.1.59 - @tryghost/string@0.3.1 - @tryghost/timezone-data@0.4.17 - @tryghost/url-utils@5.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b943e5f4cc5..03584927640 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.5.0", + "version": "1.5.1", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 6638427736864bc9a370b460691a61fd754a18bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:53:18 +0000 Subject: [PATCH 237/255] Update dependency sinon to v21.0.2 (#809) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03584927640..5fb43c12290 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "11.0.0", "mocha": "11.7.5", "should": "13.2.3", - "sinon": "21.0.1" + "sinon": "21.0.2" }, "dependencies": { "@tryghost/errors": "2.2.1", From 9abfbd445b95f4ef52c3747f9fd83f48fe19bf65 Mon Sep 17 00:00:00 2001 From: Chris Raible Date: Tue, 17 Mar 2026 20:47:03 -0700 Subject: [PATCH 238/255] Published new versions - @tryghost/adapter-base-cache@0.1.23 - @tryghost/admin-api@1.14.7 - @tryghost/admin-api-schema@4.7.2 - @tryghost/color-utils@0.2.16 - @tryghost/config-url-helpers@1.0.23 - @tryghost/content-api@1.12.6 - @tryghost/custom-fonts@1.0.8 - @tryghost/helpers@1.1.103 - @tryghost/helpers-gatsby@2.1.2 - @tryghost/image-transform@1.4.13 - @tryghost/limit-service@1.5.2 - @tryghost/referrer-parser@0.1.15 - @tryghost/schema-org@0.1.51 - @tryghost/social-urls@0.1.60 - @tryghost/string@0.3.2 - @tryghost/timezone-data@0.4.18 - @tryghost/url-utils@5.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5fb43c12290..64d53ef55e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.5.1", + "version": "1.5.2", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 059d4c4c9b8e14047b2e06d992edd3a21c985ff9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:04:29 +0000 Subject: [PATCH 239/255] Update dependency sinon to v21.0.3 (#828) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64d53ef55e7..010361202ba 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "11.0.0", "mocha": "11.7.5", "should": "13.2.3", - "sinon": "21.0.2" + "sinon": "21.0.3" }, "dependencies": { "@tryghost/errors": "2.2.1", From 3a0b57f7471c7ac968dead4641de5fea2cdbc8ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:56:58 +0000 Subject: [PATCH 240/255] Update dependency lodash to v4.18.1 [SECURITY] (#847) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 010361202ba..6dc28664303 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@tryghost/errors": "2.2.1", - "lodash": "4.17.23", + "lodash": "4.18.1", "luxon": "3.7.2" } } From 9d423ac5e244e3b7a0dc5ec323a47161ad6bb5df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:51:52 +0000 Subject: [PATCH 241/255] Update dependency sinon to v21.1.0 (#865) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6dc28664303..78fc7e015a6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "11.0.0", "mocha": "11.7.5", "should": "13.2.3", - "sinon": "21.0.3" + "sinon": "21.1.0" }, "dependencies": { "@tryghost/errors": "2.2.1", From 6d72f3a3aca08d53cae55846c78590ea05e21632 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:14:26 +0000 Subject: [PATCH 242/255] Update dependency sinon to v21.1.1 (#867) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78fc7e015a6..3fe2bfd6979 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "11.0.0", "mocha": "11.7.5", "should": "13.2.3", - "sinon": "21.1.0" + "sinon": "21.1.1" }, "dependencies": { "@tryghost/errors": "2.2.1", From 9506585f1970a0799332020c961fb62867622cdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:51:27 +0000 Subject: [PATCH 243/255] Update dependency sinon to v21.1.2 (#869) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fe2bfd6979..cebb9cfd4a8 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "11.0.0", "mocha": "11.7.5", "should": "13.2.3", - "sinon": "21.1.1" + "sinon": "21.1.2" }, "dependencies": { "@tryghost/errors": "2.2.1", From 5360f3a84545162ef19bdd65cfe15a972b45eb31 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Mon, 27 Apr 2026 12:59:24 +0100 Subject: [PATCH 244/255] Published new versions - @tryghost/adapter-base-cache@0.1.24 - @tryghost/admin-api@1.14.8 - @tryghost/admin-api-schema@4.7.3 - @tryghost/color-utils@0.2.17 - @tryghost/config-url-helpers@1.0.24 - @tryghost/content-api@1.12.7 - @tryghost/custom-fonts@1.0.9 - @tryghost/helpers@1.1.104 - @tryghost/helpers-gatsby@2.1.3 - @tryghost/html-to-plaintext@1.0.9 - @tryghost/image-transform@1.4.14 - @tryghost/limit-service@1.5.3 - @tryghost/members-csv@2.0.7 - @tryghost/referrer-parser@0.1.16 - @tryghost/schema-org@0.1.52 - @tryghost/social-urls@0.1.61 - @tryghost/string@0.3.3 - @tryghost/timezone-data@0.4.19 - @tryghost/url-utils@5.2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cebb9cfd4a8..390ee88ab12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.5.2", + "version": "1.5.3", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From bab47473c73bca8ebf02379fb5d2134696c620de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 22:42:49 +0000 Subject: [PATCH 245/255] Update dependency sinon to v22 (#903) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 390ee88ab12..077cec817aa 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "11.0.0", "mocha": "11.7.5", "should": "13.2.3", - "sinon": "21.1.2" + "sinon": "22.0.0" }, "dependencies": { "@tryghost/errors": "2.2.1", From 3ee1fc24fa2a06a162fad44a15d23b58a29df1c9 Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 12 May 2026 16:07:33 +0100 Subject: [PATCH 246/255] Published new versions - @tryghost/adapter-base-cache@0.1.25 - @tryghost/admin-api@1.14.9 - @tryghost/admin-api-schema@4.7.4 - @tryghost/color-utils@0.2.18 - @tryghost/config-url-helpers@1.0.25 - @tryghost/content-api@1.12.8 - @tryghost/custom-fonts@1.0.10 - @tryghost/helpers@1.1.105 - @tryghost/helpers-gatsby@2.1.4 - @tryghost/html-to-plaintext@1.0.10 - @tryghost/image-transform@1.4.15 - @tryghost/limit-service@1.5.4 - @tryghost/referrer-parser@0.1.17 - @tryghost/schema-org@0.1.53 - @tryghost/social-urls@0.1.62 - @tryghost/string@0.3.4 - @tryghost/timezone-data@0.4.20 - @tryghost/url-utils@5.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 077cec817aa..80d9ee94294 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.5.3", + "version": "1.5.4", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From fdb657ccba0f0244e1008d2470ccac4c0b2c3a4f Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 15 May 2026 09:57:05 +0100 Subject: [PATCH 247/255] Added publicSiteAccess flag limit (#918) - Adds `publicSiteAccess` to the limit allowlist so it can be used as a FlagLimit (`{disabled: true}`) to lock sites into private-only as needed --- lib/config.js | 3 ++- test/LimitService.test.js | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index 3b752d8f8a1..979b01cb7f8 100644 --- a/lib/config.js +++ b/lib/config.js @@ -58,5 +58,6 @@ module.exports = { }, limitStripeConnect: {}, limitAnalytics: {}, - limitSocialWeb: {} + limitSocialWeb: {}, + publicSiteAccess: {} }; diff --git a/test/LimitService.test.js b/test/LimitService.test.js index 8d1098f8a17..134a69bbfae 100644 --- a/test/LimitService.test.js +++ b/test/LimitService.test.js @@ -157,6 +157,19 @@ describe('Limit Service', function () { limitService.isLimited('limitSocialWeb').should.be.true(); }); + it('can load publicSiteAccess flag limit', function () { + const limitService = new LimitService(); + + let limits = {publicSiteAccess: {disabled: true}}; + + limitService.loadLimits({limits, errors}); + + limitService.limits.should.be.an.Object().with.properties(['publicSiteAccess']); + limitService.limits.publicSiteAccess.should.be.an.instanceOf(FlagLimit); + limitService.isLimited('publicSiteAccess').should.be.true(); + limitService.isDisabled('publicSiteAccess').should.be.true(); + }); + it('can load camel cased limits', function () { const limitService = new LimitService(); From 3da593c05b47f9981cc7cfdf0faeffcb3f07a1be Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 15 May 2026 10:18:23 +0100 Subject: [PATCH 248/255] Published new versions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80d9ee94294..0b2b1e447f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.5.4", + "version": "1.5.5", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From b2963dc629443015cad8a86cda70e184e1021c35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 14:32:01 +0000 Subject: [PATCH 249/255] Update dependency mocha to v11.7.6 (#928) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b2b1e447f7..464a226f5a9 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "c8": "11.0.0", - "mocha": "11.7.5", + "mocha": "11.7.6", "should": "13.2.3", "sinon": "22.0.0" }, From c38c7de631300121fb98cd1800d046351bd61f20 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 8 Jun 2026 10:45:51 -0500 Subject: [PATCH 250/255] Published new versions - @tryghost/adapter-base-cache@0.1.26 - @tryghost/admin-api@1.14.10 - @tryghost/admin-api-schema@4.7.5 - @tryghost/color-utils@0.2.19 - @tryghost/config-url-helpers@1.0.26 - @tryghost/content-api@1.12.9 - @tryghost/custom-fonts@1.0.11 - @tryghost/helpers@1.1.106 - @tryghost/helpers-gatsby@2.1.5 - @tryghost/html-to-plaintext@1.0.11 - @tryghost/image-transform@1.4.16 - @tryghost/limit-service@1.5.6 - @tryghost/referrer-parser@0.1.18 - @tryghost/schema-org@0.1.54 - @tryghost/social-urls@0.1.63 - @tryghost/string@0.3.5 - @tryghost/timezone-data@0.5.0 - @tryghost/url-utils@5.2.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 464a226f5a9..8458f58cd25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/limit-service", - "version": "1.5.5", + "version": "1.5.6", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/SDK.git", From 91b4e249c6bcd4eec05a43e4459b75a424dc08b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:42:04 +0000 Subject: [PATCH 251/255] Update dependency c8 to v12 (#1009) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8458f58cd25..a2683400bb3 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "access": "public" }, "devDependencies": { - "c8": "11.0.0", + "c8": "12.0.0", "mocha": "11.7.6", "should": "13.2.3", "sinon": "22.0.0" From d5656c860d842f32150b2b19db67abae8604fedb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:05:43 +0000 Subject: [PATCH 252/255] Update dependency sinon to v22.1.0 (#989) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a2683400bb3..f87f3678e26 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "c8": "12.0.0", "mocha": "11.7.6", "should": "13.2.3", - "sinon": "22.0.0" + "sinon": "22.1.0" }, "dependencies": { "@tryghost/errors": "2.2.1", From af752ecbdd3c2e0a1960730f03a1c89d1a77b086 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:17:06 +0000 Subject: [PATCH 253/255] Update dependency mocha to v11.8.0 (#1040) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f87f3678e26..34aa1486246 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "c8": "12.0.0", - "mocha": "11.7.6", + "mocha": "11.8.0", "should": "13.2.3", "sinon": "22.1.0" }, From 1c455660017aa94f2943729934535aeeef17d96c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 23:34:54 +0000 Subject: [PATCH 254/255] Update dependency mocha to v12 (#1066) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 34aa1486246..f805f803778 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "c8": "12.0.0", - "mocha": "11.8.0", + "mocha": "12.0.0", "should": "13.2.3", "sinon": "22.1.0" }, From b204579a34521fe696a6fbf4ef5d226fc83b2318 Mon Sep 17 00:00:00 2001 From: Rob Lester Date: Mon, 7 Sep 2026 12:41:47 +0100 Subject: [PATCH 255/255] Wired the imported limit-service into the workspace Only what Ghost needs to consume the package it just took ownership of: private at an internal version, repository metadata and publishing configuration pointed here, and core, the admin framework and Ember admin resolving it as a workspace package rather than from the registry. The dependency rule that grouped its version bumps into their own pull request goes too, since there are no longer any to group. Its own test script ran the linter through yarn afterwards, which this repository does not use, so that one word is changed and nothing else. The source is otherwise untouched, and stays out of the repository formatter and documentation linter for now so it can still be compared against where it came from. It is marked as mid-migration with the remaining work named: it is CommonJS, its tests are mocha, and it still holds Ghost's database queries. One thing had to change around it. The package is CommonJS, which a browser bundler only converts while pre-bundling, so consumed as workspace source rather than from the registry it reached Admin unconverted, failed to load, and left the limiter reporting every host limit as absent. It is forced through the pre-bundler until the package itself is converted, which the next change does. ref https://linear.app/ghost/issue/BER-3797 --- .github/renovate.json5 | 8 - .markdownlint-cli2.jsonc | 4 + .oxfmtrc.json | 1 + apps/admin-x-framework/package.json | 2 +- apps/admin/vite.config.ts | 6 +- apps/admin/vitest.acceptance.config.ts | 5 + apps/ember-admin/package.json | 2 +- ghost/core/package.json | 2 +- packages/limit-service/package.json | 72 ++++----- pnpm-lock.yaml | 204 ++++++++++++++++++++++--- pnpm-workspace.yaml | 1 - 11 files changed, 237 insertions(+), 70 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 166027bc81a..eb5c675dae1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -389,14 +389,6 @@ ], }, - // Bump @tryghost/limit-service on its own. Limit changes are feature-tied - // (each new flag is consumed by specific BREAD/limit-helper code) and we - // want the version bump landing as an isolated, reviewable PR rather than - // riding in with unrelated admin-support package updates. - { - groupName: '@tryghost/limit-service', - matchPackageNames: ['@tryghost/limit-service'], - }, { groupName: 'TryGhost content and email packages', matchPackageNames: [ diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index c803aea75a2..51c30eb7098 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -1,4 +1,8 @@ { + // The imported limit-service documentation is kept exactly as it arrived from + // TryGhost/SDK so it can be diffed against the source. It is linted once it is + // rewritten alongside the package. + "ignores": ["packages/limit-service/**"], "config": { "default": false, "MD011": true, diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 613f7c39ff0..083d23fd4aa 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -18,6 +18,7 @@ "koenig/kg-simplemde/debug/**", "koenig/koenig-lexical/**", "packages/i18n/locales/**", + "packages/limit-service/**", ".changeset/ledger.yaml" ] } diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index 123e18dfa77..bff042ba3d5 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -80,7 +80,7 @@ "dependencies": { "@sentry/react": "catalog:", "@tanstack/react-query": "catalog:", - "@tryghost/limit-service": "catalog:", + "@tryghost/limit-service": "workspace:*", "@tryghost/metafield-types": "workspace:*", "@tryghost/nql-string": "workspace:*", "@tryghost/shade": "workspace:*", diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts index f50de0e193e..b05c00ad99e 100644 --- a/apps/admin/vite.config.ts +++ b/apps/admin/vite.config.ts @@ -49,7 +49,11 @@ export default defineConfig(({ command }) => ({ // forwardConsole: { logLevels: ['warn', 'error'] } }, optimizeDeps: { - include: ['@tryghost/koenig-lexical'], + // limit-service is CommonJS, and Vite only converts CommonJS while pre-bundling. A + // workspace package is treated as source and served raw, where `module` does not exist, + // so the import fails and the limiter silently falls back to reporting every host limit + // as absent. Force it through the pre-bundler until the package itself is converted. + include: ['@tryghost/koenig-lexical', '@tryghost/limit-service'], }, resolve: sharedResolve, test: { diff --git a/apps/admin/vitest.acceptance.config.ts b/apps/admin/vitest.acceptance.config.ts index 9176717869e..d0be8913e00 100644 --- a/apps/admin/vitest.acceptance.config.ts +++ b/apps/admin/vitest.acceptance.config.ts @@ -34,6 +34,11 @@ export default defineConfig({ // suite. Test files and screen helpers import test-lane modules the // browser bundler can't process; vitest serves those itself. entries: ['src/**/*.{ts,tsx}', '!src/**/*.test.*', '!src/**/*.screen.ts'], + // limit-service is CommonJS, and Vite only converts CommonJS while pre-bundling. A + // workspace package is treated as source and served raw, where `module` does not exist, + // so the import fails and the limiter silently falls back to reporting every host limit + // as absent. Force it through the pre-bundler until the package itself is converted. + include: ['@tryghost/limit-service'], }, resolve: sharedResolve, test: { diff --git a/apps/ember-admin/package.json b/apps/ember-admin/package.json index 9fe5a4eccdb..f6439880676 100644 --- a/apps/ember-admin/package.json +++ b/apps/ember-admin/package.json @@ -53,7 +53,7 @@ "@tryghost/kg-clean-basic-html": "workspace:*", "@tryghost/kg-converters": "workspace:*", "@tryghost/koenig-lexical": "workspace:*", - "@tryghost/limit-service": "catalog:", + "@tryghost/limit-service": "workspace:*", "@tryghost/nql": "catalog:", "@tryghost/nql-string": "workspace:*", "@tryghost/string": "catalog:", diff --git a/ghost/core/package.json b/ghost/core/package.json index ff001258c81..7bf1f4d8b86 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -115,7 +115,7 @@ "@tryghost/kg-html-to-lexical": "workspace:*", "@tryghost/kg-lexical-html-renderer": "workspace:*", "@tryghost/kg-markdown-html-renderer": "workspace:*", - "@tryghost/limit-service": "catalog:", + "@tryghost/limit-service": "workspace:*", "@tryghost/logging": "catalog:", "@tryghost/metafield-types": "workspace:*", "@tryghost/metrics": "catalog:", diff --git a/packages/limit-service/package.json b/packages/limit-service/package.json index f805f803778..4b7ff962575 100644 --- a/packages/limit-service/package.json +++ b/packages/limit-service/package.json @@ -1,37 +1,39 @@ { - "name": "@tryghost/limit-service", - "version": "1.5.6", - "repository": { - "type": "git", - "url": "git+https://github.com/TryGhost/SDK.git", - "directory": "packages/limit-service" - }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "exports": "./index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing c8 --reporter text --reporter cobertura mocha './test/**/*.test.js'", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" - }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "c8": "12.0.0", - "mocha": "12.0.0", - "should": "13.2.3", - "sinon": "22.1.0" - }, - "dependencies": { - "@tryghost/errors": "2.2.1", - "lodash": "4.18.1", - "luxon": "3.7.2" - } + "name": "@tryghost/limit-service", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/TryGhost/Ghost.git", + "directory": "packages/limit-service" + }, + "author": "Ghost Foundation", + "license": "MIT", + "main": "index.js", + "exports": "./index.js", + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing c8 --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "lint": "eslint . --ext .js --cache", + "posttest": "pnpm run lint" + }, + "files": [ + "index.js", + "lib" + ], + "devDependencies": { + "c8": "12.0.0", + "mocha": "12.0.0", + "should": "13.2.3", + "sinon": "22.1.0" + }, + "dependencies": { + "@tryghost/errors": "2.2.1", + "lodash": "4.18.1", + "luxon": "3.7.2" + }, + "private": true, + "ghostPackage": { + "goldenPath": "migration", + "reason": "Imported from TryGhost/SDK with its history. Still CommonJS with mocha tests, and still contains Ghost's database queries. Modernised in the change that follows this one." + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03cc48f1670..ffda5f6dc09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,9 +272,6 @@ catalogs: '@tryghost/helpers': specifier: 1.1.106 version: 1.1.106 - '@tryghost/limit-service': - specifier: 1.5.6 - version: 1.5.6 '@tryghost/metrics': specifier: 3.5.3 version: 3.5.3 @@ -1128,8 +1125,8 @@ importers: specifier: 'catalog:' version: 5.101.4(react@18.3.1) '@tryghost/limit-service': - specifier: 'catalog:' - version: 1.5.6 + specifier: workspace:* + version: link:../../packages/limit-service '@tryghost/metafield-types': specifier: workspace:* version: link:../../packages/metafield-types @@ -1468,8 +1465,8 @@ importers: specifier: workspace:* version: link:../../koenig/koenig-lexical '@tryghost/limit-service': - specifier: 'catalog:' - version: 1.5.6 + specifier: workspace:* + version: link:../../packages/limit-service '@tryghost/nql': specifier: 0.13.4 version: 0.13.4(supports-color@10.2.2) @@ -1763,7 +1760,7 @@ importers: version: 6.4.1(supports-color@10.2.2) sinon-chai: specifier: 4.0.1 - version: 4.0.1(chai@4.5.0)(sinon@22.0.0) + version: 4.0.1(chai@4.5.0)(sinon@22.1.0) testem: specifier: 3.19.1 version: 3.19.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) @@ -2455,8 +2452,8 @@ importers: specifier: workspace:* version: link:../../koenig/kg-markdown-html-renderer '@tryghost/limit-service': - specifier: 'catalog:' - version: 1.5.6 + specifier: workspace:* + version: link:../../packages/limit-service '@tryghost/logging': specifier: 5.4.3 version: 5.4.3(supports-color@10.2.2) @@ -4232,6 +4229,31 @@ importers: specifier: 'catalog:' version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + packages/limit-service: + dependencies: + '@tryghost/errors': + specifier: 3.3.12 + version: 3.3.12 + lodash: + specifier: 4.18.1 + version: 4.18.1 + luxon: + specifier: 3.7.2 + version: 3.7.2 + devDependencies: + c8: + specifier: 12.0.0 + version: 12.0.0 + mocha: + specifier: 12.0.0 + version: 12.0.0 + should: + specifier: 13.2.3 + version: 13.2.3 + sinon: + specifier: 22.1.0 + version: 22.1.0 + packages/metafield-types: dependencies: zod: @@ -9649,9 +9671,6 @@ packages: '@tryghost/kg-utils@1.1.5': resolution: {integrity: sha512-SlS6C1+j6czfX61zAU38z4jC2hPKaXaOh1nN8UplKpEY2wvGTd99udtTZScl+7Fc2k9/CasTKFWErH6+KE4Qyg==} - '@tryghost/limit-service@1.5.6': - resolution: {integrity: sha512-wR+Hm2v5k2FjOUhLhfPZ0p0acCfaTQoSEhuw96uw8c15CLZtm/HKsLnxA5d3AXwYmjRo5gkYskP8UYxrCKNcoQ==} - '@tryghost/logging@5.4.3': resolution: {integrity: sha512-VXJvLI1F5EwZ+4aD+TBqC0nvs40VSc+oSMBvp+ng4hJ/DaN5W0AQXMouQ3zu+6OT1VZKvs7FPTfS1ILgN2GCsg==} @@ -11955,6 +11974,9 @@ packages: browser-process-hrtime@1.0.0: resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + browserify-aes@1.2.0: resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} @@ -12066,6 +12088,16 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + c8@12.0.0: + resolution: {integrity: sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + hasBin: true + peerDependencies: + monocart-coverage-reports: ^2 + peerDependenciesMeta: + monocart-coverage-reports: + optional: true + cacache@12.0.4: resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} @@ -12301,6 +12333,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -16238,6 +16274,10 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + is-path-inside@4.0.0: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} @@ -18154,6 +18194,11 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mocha@12.0.0: + resolution: {integrity: sha512-NYNh5IFt6WYqm9bi4601m7vix8MZdXC0DwS4gY6WhXO2RgJWhivhISVmq1oklCif18OSI9l+vx5Mdm5oh1XGiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + mocha@2.5.3: resolution: {integrity: sha512-jNt2iEk9FPmZLzL+sm4FNyOIDYXf2wUU6L4Cc8OIKK/kzgMHKPi4YhTZqG4bW4kQVdIv6wutDybRhXfdnujA1Q==} engines: {node: '>= 0.8.x'} @@ -20474,6 +20519,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + recast@0.18.10: resolution: {integrity: sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==} engines: {node: '>= 4'} @@ -21026,6 +21075,10 @@ packages: serialize-javascript@4.0.0: resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + serialize-javascript@7.1.1: + resolution: {integrity: sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==} + engines: {node: '>=20.0.0'} + serve-static@1.16.3: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} @@ -21105,6 +21158,24 @@ packages: shellwords@0.1.1: resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + should-equal@2.0.0: + resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} + + should-format@3.0.3: + resolution: {integrity: sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==} + + should-type-adaptors@1.1.0: + resolution: {integrity: sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==} + + should-type@1.4.0: + resolution: {integrity: sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==} + + should-util@1.0.1: + resolution: {integrity: sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==} + + should@13.2.3: + resolution: {integrity: sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -21162,6 +21233,9 @@ packages: sinon@22.0.0: resolution: {integrity: sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==} + sinon@22.1.0: + resolution: {integrity: sha512-n1ajF2rBWMTtEwbKcw4UdFg4nCnDdq/U6RDoxtOd7oapOlRoJ5ynwFx60owROyhDpA9QhMZi0pCO/xtmwFjG7w==} + sinon@9.2.4: resolution: {integrity: sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==} deprecated: 16.1.1 @@ -21833,6 +21907,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + test-exclude@8.0.0: + resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==} + engines: {node: 20 || >=22} + testem@3.19.1: resolution: {integrity: sha512-h9LKg7pAF3B0aqp3V3Kx8vFlB/ocB1xXvRY1YxZyIKvV/3ZUXqYadi8OD2T15VoFjUZlRrm39s9e7N8A+gYBfA==} engines: {node: '>= 7.*'} @@ -23053,6 +23131,9 @@ packages: worker-farm@1.7.0: resolution: {integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==} + workerpool@10.0.3: + resolution: {integrity: sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==} + workerpool@2.3.4: resolution: {integrity: sha512-c2EWrgB9IKHi1jbf4LG9sxKgHYOY+Ej5li6siEGtFecCXWG7eQOqATPEJ0rg1KFETXROEkErc1t5XiNrLG666Q==} @@ -29576,12 +29657,6 @@ snapshots: dependencies: semver: 7.8.5 - '@tryghost/limit-service@1.5.6': - dependencies: - '@tryghost/errors': 3.3.12 - lodash: 4.18.1 - luxon: 3.7.2 - '@tryghost/logging@5.4.3(supports-color@10.2.2)': dependencies: '@tryghost/bunyan-rotating-filestream': 0.0.18 @@ -33216,6 +33291,8 @@ snapshots: browser-process-hrtime@1.0.0: {} + browser-stdout@1.3.1: {} + browserify-aes@1.2.0: dependencies: buffer-xor: 1.0.3 @@ -33349,6 +33426,20 @@ snapshots: bytes@3.1.2: {} + c8@12.0.0: + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@istanbuljs/schema': 0.1.6 + find-up: 5.0.0 + foreground-child: 3.3.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + test-exclude: 8.0.0 + v8-to-istanbul: 9.3.0 + yargs: 18.0.0 + yargs-parser: 21.1.1 + cacache@12.0.4: dependencies: bluebird: 3.7.2 @@ -33689,6 +33780,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + chownr@1.1.4: {} chownr@2.0.0: @@ -39445,6 +39540,8 @@ snapshots: is-obj@2.0.0: {} + is-path-inside@3.0.3: {} + is-path-inside@4.0.0: {} is-plain-obj@2.1.0: {} @@ -42125,6 +42222,25 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + mocha@12.0.0: + dependencies: + browser-stdout: 1.3.1 + chokidar: 5.0.0 + debug: 4.4.3(supports-color@10.2.2) + diff: 9.0.0 + find-up: 5.0.0 + glob: 13.0.6 + is-path-inside: 3.0.3 + is-unicode-supported: 0.1.0 + js-yaml: 5.2.2 + minimatch: 10.2.5 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 7.1.1 + strip-json-comments: 5.0.3 + supports-color: 10.2.2 + workerpool: 10.0.3 + mocha@2.5.3: dependencies: commander: 2.3.0 @@ -44837,6 +44953,8 @@ snapshots: dependencies: picomatch: 2.3.2 + readdirp@5.1.1: {} + recast@0.18.10: dependencies: ast-types: 0.13.3 @@ -45599,6 +45717,8 @@ snapshots: dependencies: randombytes: 2.1.0 + serialize-javascript@7.1.1: {} + serve-static@1.16.3(supports-color@10.2.2): dependencies: encodeurl: 2.0.0 @@ -45708,6 +45828,32 @@ snapshots: shellwords@0.1.1: {} + should-equal@2.0.0: + dependencies: + should-type: 1.4.0 + + should-format@3.0.3: + dependencies: + should-type: 1.4.0 + should-type-adaptors: 1.1.0 + + should-type-adaptors@1.1.0: + dependencies: + should-type: 1.4.0 + should-util: 1.0.1 + + should-type@1.4.0: {} + + should-util@1.0.1: {} + + should@13.2.3: + dependencies: + should-equal: 2.0.0 + should-format: 3.0.3 + should-type: 1.4.0 + should-type-adaptors: 1.1.0 + should-util: 1.0.1 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -45776,10 +45922,10 @@ snapshots: dependencies: semver: 7.8.5 - sinon-chai@4.0.1(chai@4.5.0)(sinon@22.0.0): + sinon-chai@4.0.1(chai@4.5.0)(sinon@22.1.0): dependencies: chai: 4.5.0 - sinon: 22.0.0 + sinon: 22.1.0 sinon@22.0.0: dependencies: @@ -45788,6 +45934,13 @@ snapshots: '@sinonjs/samsam': 10.0.2 diff: 9.0.0 + sinon@22.1.0: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 15.4.0 + '@sinonjs/samsam': 10.0.2 + diff: 9.0.0 + sinon@9.2.4: dependencies: '@sinonjs/commons': 1.8.6 @@ -46701,6 +46854,12 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + test-exclude@8.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 13.0.6 + minimatch: 10.2.5 + testem@3.19.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@xmldom/xmldom': 0.9.10 @@ -47516,7 +47675,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - optional: true v8flags@3.2.0: dependencies: @@ -48292,6 +48450,8 @@ snapshots: dependencies: errno: 0.1.8 + workerpool@10.0.3: {} + workerpool@2.3.4: dependencies: object-assign: 4.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bc2b0d600af..8532a15e0fe 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -87,7 +87,6 @@ catalog: '@tryghost/domain-events': 3.3.13 '@tryghost/errors': 3.3.12 '@tryghost/helpers': 1.1.106 - '@tryghost/limit-service': 1.5.6 '@tryghost/logging': 5.4.3 '@tryghost/mg-clean-html': 0.12.6 '@tryghost/metrics': 3.5.3