Added BookshelfMilestoneRepository implementation (#16305)

refs
https://www.notion.so/ghost/Marketing-Milestone-email-campaigns-1d2c9dee3cfa4029863edb16092ad5c4?pvs=4

This stores the received milestones in the database.
This commit is contained in:
Aileen Booker 2023-02-22 15:53:29 +02:00 committed by GitHub
parent 7b778eabe4
commit cf7d34d862
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 215 additions and 18 deletions

View File

@ -0,0 +1,9 @@
const ghostBookshelf = require('./base');
const Milestone = ghostBookshelf.Model.extend({
tableName: 'milestones'
});
module.exports = {
Milestone: ghostBookshelf.model('Milestone', Milestone)
};

View File

@ -0,0 +1,136 @@
const {Milestone} = require('@tryghost/milestones');
/**
* @typedef {import('@tryghost/milestones/lib/MilestonesService').IMilestoneRepository} IMilestoneRepository
* @typedef {import('@tryghost/milestones/lib/MilestonesService')} Milestone
*/
/**
* @implements {IMilestoneRepository}
*/
module.exports = class BookshelfMilestoneRepository {
/** @type {Object} */
#MilestoneModel;
/** @type {import('@tryghost/domain-events')} */
#DomainEvents;
/**
* @param {object} deps
* @param {object} deps.MilestoneModel Bookshelf Model
* @param {import('@tryghost/domain-events')} deps.DomainEvents
*/
constructor(deps) {
this.#MilestoneModel = deps.MilestoneModel;
this.#DomainEvents = deps.DomainEvents;
}
#modelToMilestone(model) {
return Milestone.create({
id: model.get('id'),
type: model.get('type'),
value: model.get('value'),
currency: model.get('currency'),
createdAt: model.get('created_at'),
emailSentAt: model.get('email_sent_at')
});
}
/**
* @param {import('@tryghost/milestones/lib/Milestone')} milestone
* @returns {Promise<void>}
*/
async save(milestone) {
const data = {
id: milestone.id.toHexString(),
type: milestone.type,
value: milestone.value,
currency: milestone?.currency,
created_at: milestone?.createdAt,
email_sent_at: milestone?.emailSentAt
};
const existing = await this.#MilestoneModel.findOne({id: data.id}, {require: false});
if (!existing) {
await this.#MilestoneModel.add(data);
} else {
await this.#MilestoneModel.edit(data, {
id: data.id
});
}
for (const event of milestone.events) {
this.#DomainEvents.dispatch(event);
}
}
/**
* @param {'arr'|'members'} type
* @param {string} [currency]
*
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
*/
async getLatestByType(type, currency = 'usd') {
let milestone = null;
if (type === 'arr') {
milestone = await this.#MilestoneModel.findAll({filter: `currency:${currency}+type:arr`, order: 'created_at ASC, value DESC'}, {require: false});
} else {
milestone = await this.#MilestoneModel.findAll({filter: 'type:members', order: 'created_at ASC, value DESC'}, {require: false});
}
if (!milestone || !milestone?.models?.length) {
return null;
} else {
milestone = milestone.models?.[0];
}
return this.#modelToMilestone(milestone);
}
/**
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
*/
async getLastEmailSent() {
let milestone = await this.#MilestoneModel.findAll({filter: 'email_sent_at:-null', order: 'email_sent_at ASC'}, {require: false});
if (!milestone || !milestone?.models?.length) {
return null;
} else {
milestone = milestone.models?.[0];
}
return this.#modelToMilestone(milestone);
}
/**
* @param {number} value
* @param {string} [currency]
*
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
*/
async getByARR(value, currency = 'usd') {
// find a milestone of the ARR type by a given value
const milestone = await this.#MilestoneModel.findOne({type: 'arr', currency: currency, value: value}, {require: false});
if (!milestone) {
return null;
}
return this.#modelToMilestone(milestone);
}
/**
* @param {number} value
*
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
*/
async getByCount(value) {
// find a milestone of the members type by a given value
const milestone = await this.#MilestoneModel.findOne({type: 'members', value: value}, {require: false});
if (!milestone) {
return null;
}
return this.#modelToMilestone(milestone);
}
};

View File

@ -1,5 +1,7 @@
const DomainEvents = require('@tryghost/domain-events');
const logging = require('@tryghost/logging');
const models = require('../../models');
const BookshelfMilestoneRepository = require('./BookshelfMilestoneRepository');
const JOB_TIMEOUT = 1000 * 60 * 60 * 24 * (Math.floor(Math.random() * 4)); // 0 - 4 days;
@ -31,14 +33,15 @@ module.exports = {
const db = require('../../data/db');
const MilestoneQueries = require('./MilestoneQueries');
const {
MilestonesService,
InMemoryMilestoneRepository
} = require('@tryghost/milestones');
const {MilestonesService} = require('@tryghost/milestones');
const config = require('../../../shared/config');
const milestonesConfig = config.get('milestones');
const repository = new InMemoryMilestoneRepository({DomainEvents});
const repository = new BookshelfMilestoneRepository({
DomainEvents,
MilestoneModel: models.Milestone
});
const queries = new MilestoneQueries({db});
this.api = new MilestonesService({

View File

@ -0,0 +1,27 @@
const models = require('../../../../core/server/models');
const assert = require('assert');
const errors = require('@tryghost/errors');
describe('Unit: models/milestone', function () {
before(function () {
models.init();
});
describe('validation', function () {
describe('blank', function () {
it('throws validation error for mandatory fields', function () {
return models.Milestone.add({})
.then(function () {
throw new Error('expected ValidationError');
})
.catch(function (err) {
assert.equal(err.length, 2);
assert.equal((err[0] instanceof errors.ValidationError), true);
assert.equal((err[1] instanceof errors.ValidationError), true);
assert.match(err[0].message,/milestones\.type/);
assert.match(err[1].message,/milestones\.value/);
});
});
});
});
});

View File

@ -0,0 +1,22 @@
const assert = require('assert');
const models = require('../../../../../core/server/models');
const DomainEvents = require('@tryghost/domain-events');
describe('BookshelfMilestoneRepository', function () {
let repository;
it('Provides expected public API', async function () {
const BookshelfMilestoneRepository = require('../../../../../core/server/services/milestones/BookshelfMilestoneRepository');
repository = new BookshelfMilestoneRepository({
DomainEvents,
MilestoneModel: models.Milestone
});
assert.ok(repository.save);
assert.ok(repository.getLatestByType);
assert.ok(repository.getLastEmailSent);
assert.ok(repository.getByARR);
assert.ok(repository.getByCount);
});
});

View File

@ -105,7 +105,6 @@ module.exports = class MilestonesService {
const newMilestone = await Milestone.create(milestone);
await this.#repository.save(newMilestone);
return newMilestone;
}
@ -201,13 +200,13 @@ module.exports = class MilestonesService {
// get the closest milestone we're over now
milestone = this.#getMatchedMilestone(milestonesForCurrency.values, currentARRForCurrency.arr);
// Fetch the latest milestone for this currency
const latestMilestone = await this.#getLatestArrMilestone(defaultCurrency);
// Ensure the milestone doesn't already exist
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'arr', currency: defaultCurrency});
if (milestone && milestone > 0) {
// Fetch the latest milestone for this currency
const latestMilestone = await this.#getLatestArrMilestone(defaultCurrency);
// Ensure the milestone doesn't already exist
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'arr', currency: defaultCurrency});
if (!milestoneExists && (!latestMilestone || milestone > latestMilestone.value)) {
const meta = {
currentARR: currentARRForCurrency.arr
@ -232,13 +231,13 @@ module.exports = class MilestonesService {
// get the closest milestone we're over now
let milestone = this.#getMatchedMilestone(membersMilestones, membersCount);
// Fetch the latest achieved Members milestones
const latestMembersMilestone = await this.#getLatestMembersCountMilestone();
// Ensure the milestone doesn't already exist
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'members', currency: null});
if (milestone && milestone > 0) {
// Fetch the latest achieved Members milestones
const latestMembersMilestone = await this.#getLatestMembersCountMilestone();
// Ensure the milestone doesn't already exist
const milestoneExists = await this.#checkMilestoneExists({value: milestone, type: 'members', currency: null});
if (!milestoneExists && (!latestMembersMilestone || milestone > latestMembersMilestone.value)) {
const meta = {
currentMembers: membersCount

View File

@ -1,3 +1,4 @@
module.exports.InMemoryMilestoneRepository = require('./InMemoryMilestoneRepository');
module.exports.MilestonesService = require('./MilestonesService');
module.exports.MilestoneCreatedEvent = require('./MilestoneCreatedEvent');
module.exports.Milestone = require('./Milestone');