Skip to content

Commit 84c9e88

Browse files
committed
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.
1 parent 14dd6ab commit 84c9e88

File tree

4 files changed

+17
-142
lines changed

4 files changed

+17
-142
lines changed

packages/limit-service/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ db.transaction((transacting) => {
158158

159159
### Types of limits
160160
At the moment there are four different types of limits that limit service allows to define. These types are:
161-
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.
161+
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.
162162
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`
163163
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)
164164
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.

packages/limit-service/lib/limit.js

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ class FlagLimit extends Limit {
264264
* @param {Number} options.config.disabled - disabled/enabled flag for the limit
265265
* @param {String} options.config.error - error message to use when limit is reached
266266
* @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
268267
* @param {Object} [options.db] - instance of knex db connection that currentCountQuery can use to run state check through
269268
* @param {Object} options.errors - instance of errors compatible with GhostError errors (@tryghost/errors)
270269
*/
@@ -274,7 +273,6 @@ class FlagLimit extends Limit {
274273

275274
this.disabled = config.disabled;
276275
this.fallbackMessage = `Your plan does not support ${userFacingLimitName}. Please upgrade to enable ${userFacingLimitName}.`;
277-
this.currentCountQueryFn = config?.currentCountQuery || null;
278276
}
279277

280278
generateError() {
@@ -290,59 +288,20 @@ class FlagLimit extends Limit {
290288
}
291289

292290
/**
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+
* Flag limits are on/off so using a feature is always over the limit
296292
*/
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') {
293+
async errorIfWouldGoOverLimit() {
294+
if (this.disabled) {
314295
throw this.generateError();
315296
}
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);
335297
}
336298

337299
/**
338300
* Flag limits are on/off. They don't necessarily mean the limit wasn't possible to reach
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+
* NOTE: this method should not be relied on as it's impossible to check the limit was surpassed!
343302
*/
344-
async errorIfIsOverLimit(options = {}) {
345-
await this._isOrWouldOverLimitError(options);
303+
async errorIfIsOverLimit() {
304+
return;
346305
}
347306
}
348307

packages/limit-service/test/LimitService.test.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ describe('Limit Service', function () {
281281
(await limitService.checkIfAnyOverLimit()).should.be.true();
282282
});
283283

284-
it('Confirms when a flag limit without currentCountQuery is disabled', async function () {
284+
it('Does not check flag limits when checking if any are over limit', async function () {
285285
const limitService = new LimitService();
286286

287287
let limits = {
@@ -303,19 +303,15 @@ describe('Limit Service', function () {
303303
// },
304304
customIntegrations: {
305305
disabled: true
306-
// No currentCountQuery - will be considered over limit
307306
},
308307
limitAnalytics: {
309-
disabled: true,
310-
currentCountQuery: () => true // Feature is in use, so limit won't be exceeded (grandfathered)
308+
disabled: true
311309
},
312310
limitStripeConnect: {
313311
disabled: true
314-
// No currentCountQuery - will be considered over limit
315312
},
316313
limitSocialWeb: {
317314
disabled: true
318-
// No currentCountQuery - will be considered over limit
319315
}
320316
};
321317

@@ -326,8 +322,8 @@ describe('Limit Service', function () {
326322

327323
limitService.loadLimits({limits, errors, subscription});
328324

329-
// Should return true because customIntegrations is disabled without currentCountQuery
330-
(await limitService.checkIfAnyOverLimit()).should.be.true();
325+
// Should return false because flag limits' errorIfIsOverLimit does not throw
326+
(await limitService.checkIfAnyOverLimit()).should.be.false();
331327
});
332328

333329
it('Returns nothing if limit is not configured', async function () {

packages/limit-service/test/limit.test.js

Lines changed: 6 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,16 @@ const {MaxLimit, AllowlistLimit, FlagLimit, MaxPeriodicLimit} = require('../lib/
99

1010
describe('Limit Service', function () {
1111
describe('Flag Limit', function () {
12-
it('throws if is over limit when disabled', async function () {
12+
it('do nothing if is over limit', async function () {
13+
// NOTE: the behavior of flag limit in "is over limit" use case is flawed and should not be relied on
14+
// possible solution could be throwing an error to prevent clients from using it?
1315
const config = {
1416
disabled: true
1517
};
16-
const limit = new FlagLimit({name: 'limitFlaggy', config, errors});
18+
const limit = new FlagLimit({name: 'flaggy', config, errors});
1719

18-
try {
19-
await limit.errorIfIsOverLimit();
20-
should.fail(limit, 'Should have errored');
21-
} catch (err) {
22-
should.exist(err);
23-
should.exist(err.errorType);
24-
should.equal(err.errorType, 'HostLimitError');
25-
should.exist(err.errorDetails);
26-
should.equal(err.errorDetails.name, 'limitFlaggy');
27-
should.exist(err.message);
28-
should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.');
29-
}
20+
const result = await limit.errorIfIsOverLimit();
21+
should(result).be.undefined();
3022
});
3123

3224
it('throws if would go over limit', async function () {
@@ -51,78 +43,6 @@ describe('Limit Service', function () {
5143
should.equal(err.message, 'Your plan does not support flaggy. Please upgrade to enable flaggy.');
5244
}
5345
});
54-
55-
it('does not throw if feature is in use when currentCountQuery returns true', async function () {
56-
const config = {
57-
disabled: true,
58-
currentCountQuery: () => true
59-
};
60-
const limit = new FlagLimit({name: 'flaggy', config, errors});
61-
62-
const result = await limit.errorIfIsOverLimit();
63-
should(result).be.undefined();
64-
});
65-
66-
it('throws if feature is not in use when currentCountQuery returns false', async function () {
67-
const config = {
68-
disabled: true,
69-
currentCountQuery: () => false
70-
};
71-
const limit = new FlagLimit({name: 'limitFlaggy', config, errors});
72-
73-
try {
74-
await limit.errorIfIsOverLimit();
75-
should.fail(limit, 'Should have errored');
76-
} catch (err) {
77-
should.exist(err);
78-
should.equal(err.errorType, 'HostLimitError');
79-
}
80-
});
81-
82-
it('calls currentCountQuery with transacting option', async function () {
83-
const currentCountQueryStub = sinon.stub().resolves(true);
84-
const config = {
85-
disabled: true,
86-
currentCountQuery: currentCountQueryStub
87-
};
88-
const db = {
89-
knex: 'connection'
90-
};
91-
const limit = new FlagLimit({name: 'flaggy', config, db, errors});
92-
const transaction = 'transaction';
93-
94-
await limit.errorIfIsOverLimit({transacting: transaction});
95-
96-
sinon.assert.calledOnce(currentCountQueryStub);
97-
sinon.assert.calledWithExactly(currentCountQueryStub, transaction);
98-
});
99-
100-
it('errorIfWouldGoOverLimit behaves the same as errorIfIsOverLimit', async function () {
101-
const config = {
102-
disabled: true,
103-
currentCountQuery: () => true
104-
};
105-
const limit = new FlagLimit({name: 'flaggy', config, errors});
106-
107-
// Should not throw when feature is in use
108-
const result = await limit.errorIfWouldGoOverLimit();
109-
should(result).be.undefined();
110-
111-
// Should throw when feature is not in use
112-
const config2 = {
113-
disabled: true,
114-
currentCountQuery: () => false
115-
};
116-
const limit2 = new FlagLimit({name: 'limitFlaggy', config: config2, errors});
117-
118-
try {
119-
await limit2.errorIfWouldGoOverLimit();
120-
should.fail(limit2, 'Should have errored');
121-
} catch (err) {
122-
should.exist(err);
123-
should.equal(err.errorType, 'HostLimitError');
124-
}
125-
});
12646
});
12747

12848
describe('Max Limit', function () {

0 commit comments

Comments
 (0)