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:
parent
7b778eabe4
commit
cf7d34d862
9
ghost/core/core/server/models/milestone.js
Normal file
9
ghost/core/core/server/models/milestone.js
Normal file
@ -0,0 +1,9 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
|
||||
const Milestone = ghostBookshelf.Model.extend({
|
||||
tableName: 'milestones'
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Milestone: ghostBookshelf.model('Milestone', Milestone)
|
||||
};
|
@ -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);
|
||||
}
|
||||
};
|
@ -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({
|
||||
|
27
ghost/core/test/unit/server/models/milestone.test.js
Normal file
27
ghost/core/test/unit/server/models/milestone.test.js
Normal 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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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
|
||||
|
@ -1,3 +1,4 @@
|
||||
module.exports.InMemoryMilestoneRepository = require('./InMemoryMilestoneRepository');
|
||||
module.exports.MilestonesService = require('./MilestonesService');
|
||||
module.exports.MilestoneCreatedEvent = require('./MilestoneCreatedEvent');
|
||||
module.exports.Milestone = require('./Milestone');
|
||||
|
Loading…
Reference in New Issue
Block a user