Ghost/ghost/tiers/lib/TiersAPI.js
Fabien 'egg' O'Carroll 5a5ddcb609
🐛 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.
2024-03-13 00:25:42 +07:00

172 lines
4.6 KiB
JavaScript

const ObjectID = require('bson-objectid').default;
const {BadRequestError, IncorrectUsageError} = require('@tryghost/errors');
const Tier = require('./Tier');
/**
* @typedef {object} ITierRepository
* @prop {(id: ObjectID) => Promise<Tier>} getById
* @prop {(tier: Tier) => Promise<void>} save
* @prop {(options?: {filter?: string}) => Promise<Tier[]>} getAll
*/
/**
* @typedef {object} ISlugService
* @prop {(input: string) => Promise<string>} generate
*/
/**
* @template {Model}
* @typedef {object} Page<Model>
* @prop {Model[]} data
* @prop {object} meta
* @prop {object} meta.pagination
* @prop {number} meta.pagination.page - The current page
* @prop {number} meta.pagination.pages - The total number of pages
* @prop {number} meta.pagination.limit - The limit of models per page
* @prop {number} meta.pagination.total - The totaL number of models across all pages
* @prop {number|null} meta.pagination.prev - The number of the previous page, or null if there isn't one
* @prop {number|null} meta.pagination.next - The number of the next page, or null if there isn't one
*/
module.exports = class TiersAPI {
/** @type {ITierRepository} */
#repository;
/** @type {ISlugService} */
#slugService;
constructor(deps) {
this.#repository = deps.repository;
this.#slugService = deps.slugService;
}
/**
* @param {object} [options]
* @param {any} [options.filter] - A mongo query object
*
* @returns {Promise<Page<Tier>>}
*/
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);
return {
data: tiers,
meta: {
pagination: {
page: 1,
pages: 1,
limit: tiers.length,
total: tiers.length,
prev: null,
next: null
}
}
};
}
/**
* @param {string} idString
*
* @returns {Promise<Tier>}
*/
async read(idString) {
const id = ObjectID.createFromHexString(idString);
const tier = await this.#repository.getById(id);
return tier;
}
/**
* Fetches the default tier
* @param {object} [options]
* @returns {Promise<Tier>}
*/
async readDefaultTier(options = {}) {
const [defaultTier] = await this.#repository.getAll({
filter: {
$and: [
{type: 'paid'},
{active: true}
]
},
limit: 1,
...options
});
return defaultTier;
}
/**
* @param {string} id
* @param {object} data
* @returns {Promise<Tier>}
*/
async edit(idString, data) {
const id = ObjectID.createFromHexString(idString);
const tier = await this.#repository.getById(id);
const editableProperties = [
'name',
'benefits',
'description',
'visibility',
'status',
'trialDays',
'welcomePageURL'
];
for (const editableProperty of editableProperties) {
if (Reflect.has(data, editableProperty)) {
tier[editableProperty] = data[editableProperty];
}
}
tier.updatePricing({
currency: data.currency || tier.currency,
monthlyPrice: data.monthlyPrice || tier.monthlyPrice,
yearlyPrice: data.yearlyPrice || tier.yearlyPrice
});
await this.#repository.save(tier);
return tier;
}
/**
* @param {object} data
* @returns {Promise<Tier>}
*/
async add(data) {
if (data.type === 'free') {
throw new BadRequestError({
message: 'Cannot create free Tier'
});
}
const slug = await this.#slugService.generate(data.slug || data.name);
const tier = await Tier.create({
slug,
type: 'paid',
status: 'active',
visibility: data.visibility,
name: data.name,
description: data.description,
benefits: data.benefits,
welcomePageURL: data.welcomePageURL,
monthlyPrice: data.monthlyPrice,
yearlyPrice: data.yearlyPrice,
currency: data.currency,
trialDays: data.trialDays
});
await this.#repository.save(tier);
return tier;
}
};