@tryghost/limit-service 1.2.18 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/lib/config.js +5 -11
- package/lib/limit.js +49 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
This module is intended to hold **all of the logic** for testing if site:
|
|
3
3
|
- would be over a given limit if they took an action (i.e. added one more thing, switched to a different limit)
|
|
4
4
|
- if they are over a limit already
|
|
5
|
-
- consistent error messages explaining why the limit has been reached
|
|
5
|
+
- consistent error messages explaining why the limit has been reached
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -25,7 +25,8 @@ const LimitService = require('@tryghost/limit-service');
|
|
|
25
25
|
const limitService = new LimitService();
|
|
26
26
|
|
|
27
27
|
// setup limit configuration
|
|
28
|
-
// currently supported limit keys are: staff, members, customThemes, customIntegrations, uploads
|
|
28
|
+
// currently supported limit keys are: staff, members, customThemes, customIntegrations, uploads,
|
|
29
|
+
// limitStripeConnect, limitAnalytics, and limitActivityPub
|
|
29
30
|
// all limit configs support custom "error" configuration that is a template string
|
|
30
31
|
const limits = {
|
|
31
32
|
// staff and member are "max" type of limits accepting "max" configuration
|
|
@@ -54,7 +55,7 @@ const limits = {
|
|
|
54
55
|
error: 'Email sending has been temporarily disabled whilst your account is under review.'
|
|
55
56
|
},
|
|
56
57
|
// following is a "max periodic" type of configuration
|
|
57
|
-
// note if you use this configuration, the limit service has to also get a
|
|
58
|
+
// note if you use this configuration, the limit service has to also get a
|
|
58
59
|
// "subscription" parameter to work as expected
|
|
59
60
|
// emails: {
|
|
60
61
|
// maxPeriodic: 42,
|
|
@@ -65,11 +66,14 @@ const limits = {
|
|
|
65
66
|
max: 5000000,
|
|
66
67
|
// formatting of the {{ max }} vairable is in MB, e.g: 5MB
|
|
67
68
|
error: 'Your plan supports uploads of max size up to {{max}}. Please upgrade to reenable uploading.'
|
|
68
|
-
}
|
|
69
|
+
},
|
|
70
|
+
limitStripeConnect: {},
|
|
71
|
+
limitAnalytics: {},
|
|
72
|
+
limitActivityPub: {}
|
|
69
73
|
};
|
|
70
74
|
|
|
71
75
|
// This information is needed for the limit service to work with "max periodic" limits
|
|
72
|
-
// The interval value has to be 'month' as
|
|
76
|
+
// The interval value has to be 'month' as that's the only interval that was needed for
|
|
73
77
|
// current usecase
|
|
74
78
|
// The startDate has to be in ISO 8601 format (https://en.wikipedia.org/wiki/ISO_8601)
|
|
75
79
|
const subscription = {
|
|
@@ -154,15 +158,15 @@ db.transaction((transacting) => {
|
|
|
154
158
|
|
|
155
159
|
### Types of limits
|
|
156
160
|
At the moment there are four different types of limits that limit service allows to define. These types are:
|
|
157
|
-
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.
|
|
158
|
-
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`,
|
|
161
|
+
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.
|
|
162
|
+
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`
|
|
159
163
|
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)
|
|
160
|
-
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.
|
|
164
|
+
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.
|
|
161
165
|
|
|
162
166
|
### Supported limits
|
|
163
|
-
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`.
|
|
167
|
+
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`.
|
|
164
168
|
|
|
165
|
-
All limits can act as `flag` or `allowList` types. Only certain (`members`, `staff
|
|
169
|
+
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.
|
|
166
170
|
|
|
167
171
|
### Frontend usage
|
|
168
172
|
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:
|
|
@@ -217,6 +221,6 @@ Follow the instructions for the top-level repo.
|
|
|
217
221
|
|
|
218
222
|
|
|
219
223
|
|
|
220
|
-
# Copyright & License
|
|
224
|
+
# Copyright & License
|
|
221
225
|
|
|
222
226
|
Copyright (c) 2013-2025 Ghost Foundation - Released under the [MIT license](LICENSE).
|
package/lib/config.js
CHANGED
|
@@ -45,16 +45,7 @@ module.exports = {
|
|
|
45
45
|
return result.length;
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
|
-
customIntegrations: {
|
|
49
|
-
currentCountQuery: async (knex) => {
|
|
50
|
-
let result = await knex('integrations')
|
|
51
|
-
.count('id', {as: 'count'})
|
|
52
|
-
.whereNotIn('type', ['internal', 'builtin'])
|
|
53
|
-
.first();
|
|
54
|
-
|
|
55
|
-
return result.count;
|
|
56
|
-
}
|
|
57
|
-
},
|
|
48
|
+
customIntegrations: {},
|
|
58
49
|
customThemes: {},
|
|
59
50
|
uploads: {
|
|
60
51
|
// NOTE: this function should not ever be used as for uploads we compare the size
|
|
@@ -64,5 +55,8 @@ module.exports = {
|
|
|
64
55
|
// NOTE: the uploads limit is based on file sizes provided in Bytes
|
|
65
56
|
// a custom formatter is here for more user-friendly formatting when forming an error
|
|
66
57
|
formatter: count => `${count / 1000000}MB`
|
|
67
|
-
}
|
|
58
|
+
},
|
|
59
|
+
limitStripeConnect: {},
|
|
60
|
+
limitAnalytics: {},
|
|
61
|
+
limitActivityPub: {}
|
|
68
62
|
};
|
package/lib/limit.js
CHANGED
|
@@ -264,14 +264,17 @@ class FlagLimit extends Limit {
|
|
|
264
264
|
* @param {Number} options.config.disabled - disabled/enabled flag for the limit
|
|
265
265
|
* @param {String} options.config.error - error message to use when limit is reached
|
|
266
266
|
* @param {String} options.helpLink - URL to the resource explaining how the limit works
|
|
267
|
+
* @param {Function} [options.config.currentCountQuery] - query checking the state that would be compared against the limit
|
|
267
268
|
* @param {Object} [options.db] - instance of knex db connection that currentCountQuery can use to run state check through
|
|
268
269
|
* @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors)
|
|
269
270
|
*/
|
|
270
271
|
constructor({name, config, helpLink, db, errors}) {
|
|
271
272
|
super({name, error: config.error || '', helpLink, db, errors});
|
|
273
|
+
const userFacingLimitName = lowerCase(name.replace(/^limit/, ''));
|
|
272
274
|
|
|
273
275
|
this.disabled = config.disabled;
|
|
274
|
-
this.fallbackMessage = `Your plan does not support ${
|
|
276
|
+
this.fallbackMessage = `Your plan does not support ${userFacingLimitName}. Please upgrade to enable ${userFacingLimitName}.`;
|
|
277
|
+
this.currentCountQueryFn = config?.currentCountQuery || null;
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
generateError() {
|
|
@@ -287,20 +290,59 @@ class FlagLimit extends Limit {
|
|
|
287
290
|
}
|
|
288
291
|
|
|
289
292
|
/**
|
|
290
|
-
*
|
|
293
|
+
* @param {Object} [options]
|
|
294
|
+
* @param {Object} [options.transacting] Transaction to run the count query on
|
|
295
|
+
* @returns {Promise<boolean>} - returns the current count of items that would be compared against the limit
|
|
291
296
|
*/
|
|
292
|
-
async
|
|
293
|
-
if (this.
|
|
297
|
+
async currentCountQuery(options = {}) {
|
|
298
|
+
if (!this.currentCountQueryFn || typeof this.currentCountQueryFn !== 'function') {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return await this.currentCountQueryFn(options.transacting ?? this.db?.knex);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// As Flag limits are on/off, we won't check against max values.
|
|
306
|
+
// `errorIfWouldGoOverLimit` and `errorIfIsOverLimit` end up doing the same thing.
|
|
307
|
+
async _isOrWouldOverLimitError(options = {}) {
|
|
308
|
+
if (!this.disabled) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// If no currentCountQuery is provided, throw error when disabled
|
|
313
|
+
if (!this.currentCountQueryFn || typeof this.currentCountQueryFn !== 'function') {
|
|
294
314
|
throw this.generateError();
|
|
295
315
|
}
|
|
316
|
+
|
|
317
|
+
// If currentCountQuery is provided, check if feature is in use
|
|
318
|
+
const featureInUse = await this.currentCountQuery(options);
|
|
319
|
+
|
|
320
|
+
// Only throw error if feature is NOT in use (allowing grandfathering)
|
|
321
|
+
if (!featureInUse) {
|
|
322
|
+
throw this.generateError();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Flag limits are usually on/off so using a feature is always over the limit,
|
|
328
|
+
* unless the limit has a currentCountQuery function provided to check if the
|
|
329
|
+
* feature is in use. This is a use case for when we introduce a new limit and
|
|
330
|
+
* customers have already been using this feature. We don't want to take it
|
|
331
|
+
* away from them.
|
|
332
|
+
*/
|
|
333
|
+
async errorIfWouldGoOverLimit(options = {}) {
|
|
334
|
+
await this._isOrWouldOverLimitError(options);
|
|
296
335
|
}
|
|
297
336
|
|
|
298
337
|
/**
|
|
299
338
|
* Flag limits are on/off. They don't necessarily mean the limit wasn't possible to reach
|
|
300
|
-
*
|
|
339
|
+
* Exception: the limit has a currentCountQuery function provided to check if the
|
|
340
|
+
* feature is in use. This is a use case for when we introduce a new limit and
|
|
341
|
+
* customers have already been using this feature. We don't want to take it
|
|
342
|
+
* away from them.
|
|
301
343
|
*/
|
|
302
|
-
async errorIfIsOverLimit() {
|
|
303
|
-
|
|
344
|
+
async errorIfIsOverLimit(options = {}) {
|
|
345
|
+
await this._isOrWouldOverLimitError(options);
|
|
304
346
|
}
|
|
305
347
|
}
|
|
306
348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/limit-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/TryGhost/SDK.git",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"c8": "10.1.3",
|
|
28
28
|
"mocha": "11.2.2",
|
|
29
29
|
"should": "13.2.3",
|
|
30
|
-
"sinon": "
|
|
30
|
+
"sinon": "21.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@tryghost/errors": "^1.2.26",
|
|
34
34
|
"lodash": "^4.17.21",
|
|
35
35
|
"luxon": "^1.26.0"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "a4329bd0bf75c10a862bb1c8761b45a6b08c5c2a"
|
|
38
38
|
}
|