🐛 Fixed Tiers API erroring when invalid filter passed (#19845)

closes ENG-730
closes https://linear.app/tryghost/issue/ENG-730/

We've updated the input serializer to parse the filter, and responded
with an error if it cannot be parsed correctly.

Now that it's parsed, we can pass a mongo query object through the
stack, which will lend itself to better typing for this code, which is a
direction we want to go in anyway. We've had to update all the internal
usages of the `browse` method to use mongo query objects.
This commit is contained in:
Fabien 'egg' O'Carroll 2024-03-13 00:25:42 +07:00 committed by GitHub
parent 36f11a65a0
commit 5a5ddcb609
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 50 additions and 8 deletions

View File

@ -1,10 +1,26 @@
const {BadRequestError} = require('@tryghost/errors');
const localUtils = require('../../index'); const localUtils = require('../../index');
const nql = require('@tryghost/nql-lang');
const tpl = require('@tryghost/tpl');
const messages = {
invalidNQLFilter: 'The NQL filter you passed was invalid.'
};
const forceActiveFilter = (frame) => { const forceActiveFilter = (frame) => {
if (frame.options.filter) { if (frame.options.filter) {
frame.options.filter = `(${frame.options.filter})+active:true`; frame.options.filter = {
$and: [
{
active: true
},
frame.options.filter
]
};
} else { } else {
frame.options.filter = 'active:true'; frame.options.filter = {
active: true
};
} }
}; };
@ -41,6 +57,18 @@ function convertTierInput(input) {
module.exports = { module.exports = {
all(_apiConfig, frame) { all(_apiConfig, frame) {
if (frame.options.filter) {
try {
frame.options.filter = nql.parse(frame.options.filter);
} catch (err) {
throw new BadRequestError({
message: tpl(messages.invalidNQLFilter)
});
}
} else {
frame.options.filter = null;
}
if (localUtils.isContentAPI(frame)) { if (localUtils.isContentAPI(frame)) {
// CASE: content api can only have active tiers // CASE: content api can only have active tiers
forceActiveFilter(frame); forceActiveFilter(frame);

View File

@ -55,7 +55,9 @@ const initMembersCSVImporter = ({stripeAPIService}) => {
}, },
getTierByName: async (name) => { getTierByName: async (name) => {
const tiers = await tiersService.api.browse({ const tiers = await tiersService.api.browse({
filter: `name:'${name}'` filter: {
name
}
}); });
if (tiers.data.length > 0) { if (tiers.data.length > 0) {

View File

@ -86,7 +86,8 @@ module.exports = class TierRepository {
* @returns {Promise<import('@tryghost/tiers/lib/Tier')[]>} * @returns {Promise<import('@tryghost/tiers/lib/Tier')[]>}
*/ */
async getAll(options = {}) { async getAll(options = {}) {
const filter = nql(options.filter, {}); const filter = nql();
filter.filter = options.filter || {};
return Promise.all(this.#store.slice().filter((item) => { return Promise.all(this.#store.slice().filter((item) => {
return filter.queryJSON(this.toPrimitive(item)); return filter.queryJSON(this.toPrimitive(item));
}).map((tier) => { }).map((tier) => {

View File

@ -55,7 +55,8 @@ class InMemoryTierRepository {
* @returns {Promise<Tier[]>} * @returns {Promise<Tier[]>}
*/ */
async getAll(options = {}) { async getAll(options = {}) {
const filter = nql(options.filter, {}); const filter = nql();
filter.filter = options.filter || {};
return this.#store.slice().filter((item) => { return this.#store.slice().filter((item) => {
return filter.queryJSON(this.toPrimitive(item)); return filter.queryJSON(this.toPrimitive(item));
}); });

View File

@ -1,5 +1,5 @@
const ObjectID = require('bson-objectid').default; const ObjectID = require('bson-objectid').default;
const {BadRequestError} = require('@tryghost/errors'); const {BadRequestError, IncorrectUsageError} = require('@tryghost/errors');
const Tier = require('./Tier'); const Tier = require('./Tier');
/** /**
@ -42,11 +42,16 @@ module.exports = class TiersAPI {
/** /**
* @param {object} [options] * @param {object} [options]
* @param {string} [options.filter] - An NQL filter string * @param {any} [options.filter] - A mongo query object
* *
* @returns {Promise<Page<Tier>>} * @returns {Promise<Page<Tier>>}
*/ */
async browse(options = {}) { async browse(options = {}) {
if (typeof options.filter === 'string') {
throw new IncorrectUsageError({
message: 'filter must be a mongo query object'
});
}
const tiers = await this.#repository.getAll(options); const tiers = await this.#repository.getAll(options);
return { return {
@ -83,7 +88,12 @@ module.exports = class TiersAPI {
*/ */
async readDefaultTier(options = {}) { async readDefaultTier(options = {}) {
const [defaultTier] = await this.#repository.getAll({ const [defaultTier] = await this.#repository.getAll({
filter: 'type:paid+active:true', filter: {
$and: [
{type: 'paid'},
{active: true}
]
},
limit: 1, limit: 1,
...options ...options
}); });