mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
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
7 changed files with 215 additions and 18 deletions
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 DomainEvents = require('@tryghost/domain-events');
|
||||||
const logging = require('@tryghost/logging');
|
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;
|
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 db = require('../../data/db');
|
||||||
const MilestoneQueries = require('./MilestoneQueries');
|
const MilestoneQueries = require('./MilestoneQueries');
|
||||||
|
|
||||||
const {
|
const {MilestonesService} = require('@tryghost/milestones');
|
||||||
MilestonesService,
|
|
||||||
InMemoryMilestoneRepository
|
|
||||||
} = require('@tryghost/milestones');
|
|
||||||
const config = require('../../../shared/config');
|
const config = require('../../../shared/config');
|
||||||
const milestonesConfig = config.get('milestones');
|
const milestonesConfig = config.get('milestones');
|
||||||
|
|
||||||
const repository = new InMemoryMilestoneRepository({DomainEvents});
|
const repository = new BookshelfMilestoneRepository({
|
||||||
|
DomainEvents,
|
||||||
|
MilestoneModel: models.Milestone
|
||||||
|
});
|
||||||
|
|
||||||
const queries = new MilestoneQueries({db});
|
const queries = new MilestoneQueries({db});
|
||||||
|
|
||||||
this.api = new MilestonesService({
|
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);
|
const newMilestone = await Milestone.create(milestone);
|
||||||
|
|
||||||
await this.#repository.save(newMilestone);
|
await this.#repository.save(newMilestone);
|
||||||
|
|
||||||
return newMilestone;
|
return newMilestone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,13 +200,13 @@ module.exports = class MilestonesService {
|
||||||
// get the closest milestone we're over now
|
// get the closest milestone we're over now
|
||||||
milestone = this.#getMatchedMilestone(milestonesForCurrency.values, currentARRForCurrency.arr);
|
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) {
|
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)) {
|
if (!milestoneExists && (!latestMilestone || milestone > latestMilestone.value)) {
|
||||||
const meta = {
|
const meta = {
|
||||||
currentARR: currentARRForCurrency.arr
|
currentARR: currentARRForCurrency.arr
|
||||||
|
@ -232,13 +231,13 @@ module.exports = class MilestonesService {
|
||||||
// get the closest milestone we're over now
|
// get the closest milestone we're over now
|
||||||
let milestone = this.#getMatchedMilestone(membersMilestones, membersCount);
|
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) {
|
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)) {
|
if (!milestoneExists && (!latestMembersMilestone || milestone > latestMembersMilestone.value)) {
|
||||||
const meta = {
|
const meta = {
|
||||||
currentMembers: membersCount
|
currentMembers: membersCount
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
module.exports.InMemoryMilestoneRepository = require('./InMemoryMilestoneRepository');
|
module.exports.InMemoryMilestoneRepository = require('./InMemoryMilestoneRepository');
|
||||||
module.exports.MilestonesService = require('./MilestonesService');
|
module.exports.MilestonesService = require('./MilestonesService');
|
||||||
module.exports.MilestoneCreatedEvent = require('./MilestoneCreatedEvent');
|
module.exports.MilestoneCreatedEvent = require('./MilestoneCreatedEvent');
|
||||||
|
module.exports.Milestone = require('./Milestone');
|
||||||
|
|
Loading…
Add table
Reference in a new issue