Added MilestoneCreatedEvent using DomainEvents
no issue - In preparation of using event emitting for Milestone achievements, we needed to add a dedicated `MilestoneCreatedEvent` to the `Milestone` entity. - The event will be emitted using `DomainEvents` when a new milesteone is saved, which will allow us to listen to these events.
This commit is contained in:
parent
636ff6d4ef
commit
513b7d1df4
@ -1,3 +1,5 @@
|
||||
const DomainEvents = require('@tryghost/domain-events');
|
||||
|
||||
const getStripeLiveEnabled = () => {
|
||||
const settingsCache = require('../../../shared/settings-cache');
|
||||
const stripeConnect = settingsCache.get('stripe_connect_publishable_key');
|
||||
@ -35,7 +37,7 @@ module.exports = {
|
||||
const {GhostMailer} = require('../mail');
|
||||
|
||||
const mailer = new GhostMailer();
|
||||
const repository = new InMemoryMilestoneRepository();
|
||||
const repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
const queries = new MilestoneQueries({db});
|
||||
|
||||
this.api = new MilestonesService({
|
||||
|
@ -13,6 +13,17 @@ module.exports = class InMemoryMilestoneRepository {
|
||||
/** @type {Object.<string, true>} */
|
||||
#ids = {};
|
||||
|
||||
/** @type {import('@tryghost/domain-events')} */
|
||||
#DomainEvents;
|
||||
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {import('@tryghost/domain-events')} deps.DomainEvents
|
||||
*/
|
||||
constructor(deps) {
|
||||
this.#DomainEvents = deps.DomainEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Milestone} milestone
|
||||
*
|
||||
@ -27,6 +38,10 @@ module.exports = class InMemoryMilestoneRepository {
|
||||
} else {
|
||||
this.#store.push(milestone);
|
||||
this.#ids[milestone.id.toHexString()] = true;
|
||||
|
||||
for (const event of milestone.events) {
|
||||
this.#DomainEvents.dispatch(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
const ObjectID = require('bson-objectid').default;
|
||||
const {ValidationError} = require('@tryghost/errors');
|
||||
const MilestoneCreatedEvent = require('./MilestoneCreatedEvent');
|
||||
|
||||
module.exports = class Milestone {
|
||||
/** @type {Array} */
|
||||
events = [];
|
||||
|
||||
/**
|
||||
* @type {ObjectID}
|
||||
*/
|
||||
@ -79,8 +83,22 @@ module.exports = class Milestone {
|
||||
* @returns {Promise<Milestone>}
|
||||
*/
|
||||
static async create(data) {
|
||||
// order of validation matters!
|
||||
const id = validateId(data.id);
|
||||
/** @type ObjectID */
|
||||
let id;
|
||||
let isNew = false;
|
||||
if (!data.id) {
|
||||
isNew = true;
|
||||
id = new ObjectID();
|
||||
} else if (typeof data.id === 'string') {
|
||||
id = ObjectID.createFromHexString(data.id);
|
||||
} else if (data.id instanceof ObjectID) {
|
||||
id = data.id;
|
||||
} else {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid ID provided for Milestone'
|
||||
});
|
||||
}
|
||||
|
||||
const type = validateType(data.type);
|
||||
const currency = validateCurrency(type, data?.currency);
|
||||
const value = validateValue(data.value);
|
||||
@ -102,7 +120,7 @@ module.exports = class Milestone {
|
||||
createdAt = new Date();
|
||||
}
|
||||
|
||||
return new Milestone({
|
||||
const milestone = new Milestone({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
@ -111,28 +129,15 @@ module.exports = class Milestone {
|
||||
createdAt,
|
||||
emailSentAt
|
||||
});
|
||||
|
||||
if (isNew) {
|
||||
milestone.events.push(MilestoneCreatedEvent.create({milestone}));
|
||||
}
|
||||
|
||||
return milestone;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ObjectID|string|null} id
|
||||
*
|
||||
* @returns {ObjectID}
|
||||
*/
|
||||
function validateId(id) {
|
||||
if (!id) {
|
||||
return new ObjectID();
|
||||
}
|
||||
if (typeof id === 'string') {
|
||||
return ObjectID.createFromHexString(id);
|
||||
}
|
||||
if (id instanceof ObjectID) {
|
||||
return id;
|
||||
}
|
||||
return new ObjectID();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number|null} value
|
||||
|
22
ghost/milestones/lib/MilestoneCreatedEvent.js
Normal file
22
ghost/milestones/lib/MilestoneCreatedEvent.js
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @typedef {object} MilestoneCreatedEventData
|
||||
*/
|
||||
|
||||
module.exports = class MilestoneCreatedEvent {
|
||||
/**
|
||||
* @param {MilestoneCreatedEventData} data
|
||||
* @param {Date} timestamp
|
||||
*/
|
||||
constructor(data, timestamp) {
|
||||
this.data = data;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MilestoneCreatedEventData} data
|
||||
* @param {Date} [timestamp]
|
||||
*/
|
||||
static create(data, timestamp) {
|
||||
return new MilestoneCreatedEvent(data, timestamp ?? new Date);
|
||||
}
|
||||
};
|
@ -2,13 +2,17 @@ const assert = require('assert');
|
||||
const ObjectID = require('bson-objectid');
|
||||
const InMemoryMilestoneRepository = require('../lib/InMemoryMilestoneRepository');
|
||||
const Milestone = require('../lib/Milestone');
|
||||
const DomainEvents = require('@tryghost/domain-events');
|
||||
const sinon = require('sinon');
|
||||
|
||||
describe('InMemoryMilestoneRepository', function () {
|
||||
let repository;
|
||||
let domainEventsSpy;
|
||||
|
||||
before(async function () {
|
||||
const resourceId = new ObjectID();
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
domainEventsSpy = sinon.spy(DomainEvents, 'dispatch');
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
const milestoneCreatePromises = [];
|
||||
|
||||
const validInputs = [
|
||||
@ -16,7 +20,7 @@ describe('InMemoryMilestoneRepository', function () {
|
||||
type: 'arr',
|
||||
value: 20000,
|
||||
createdAt: '2023-01-01T00:00:00Z',
|
||||
id: resourceId
|
||||
id: resourceId // duplicate id
|
||||
},
|
||||
{
|
||||
type: 'arr',
|
||||
@ -49,7 +53,7 @@ describe('InMemoryMilestoneRepository', function () {
|
||||
value: 100,
|
||||
createdAt: '2023-01-01T00:00:00Z',
|
||||
emailSentAt: '2023-01-01T00:00:00Z',
|
||||
id: resourceId
|
||||
id: resourceId // duplicate id
|
||||
},
|
||||
{
|
||||
type: 'members',
|
||||
@ -74,6 +78,14 @@ describe('InMemoryMilestoneRepository', function () {
|
||||
}
|
||||
});
|
||||
|
||||
after(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Can dispatch events when saving a new Milestone', async function () {
|
||||
assert(domainEventsSpy.callCount === 6);
|
||||
});
|
||||
|
||||
it('Can return the latest milestone for members', async function () {
|
||||
const latestMemberCountMilestone = await repository.getLatestByType('members');
|
||||
const timeDiff = new Date(latestMemberCountMilestone.createdAt) - new Date('2023-02-01T00:00:00.000Z');
|
||||
|
@ -33,7 +33,8 @@ describe('Milestone', function () {
|
||||
describe('create', function () {
|
||||
it('Will error with invalid inputs', async function () {
|
||||
const invalidInputs = [
|
||||
{id: 'Not valid ID'},
|
||||
{id: 'Invalid ID provided for Milestone'},
|
||||
{id: 124},
|
||||
{value: 'Invalid Value'},
|
||||
{createdAt: 'Invalid Date'},
|
||||
{emailSentAt: 'Invalid Date'}
|
||||
@ -63,7 +64,8 @@ describe('Milestone', function () {
|
||||
it('Will not error with valid inputs', async function () {
|
||||
const validInputs = [
|
||||
{id: new ObjectID()},
|
||||
{id: 123},
|
||||
{id: new ObjectID().toString()},
|
||||
{id: null},
|
||||
{type: 'something'},
|
||||
{name: 'testing'},
|
||||
{name: 'members-10000000'},
|
||||
@ -110,5 +112,15 @@ describe('Milestone', function () {
|
||||
|
||||
assert(milestone.name === 'members-100');
|
||||
});
|
||||
|
||||
it('Will create event for new milestone', async function () {
|
||||
const milestone = await Milestone.create({
|
||||
...validInputMembers,
|
||||
value: 500,
|
||||
type: 'members'
|
||||
});
|
||||
|
||||
assert.ok(milestone.events);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,9 +4,20 @@ const {
|
||||
InMemoryMilestoneRepository
|
||||
} = require('../index');
|
||||
const Milestone = require('../lib/Milestone');
|
||||
const DomainEvents = require('@tryghost/domain-events');
|
||||
const sinon = require('sinon');
|
||||
|
||||
describe('MilestonesService', function () {
|
||||
let repository;
|
||||
let domainEventsSpy;
|
||||
|
||||
beforeEach(async function () {
|
||||
domainEventsSpy = sinon.spy(DomainEvents, 'dispatch');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
const milestonesConfig = {
|
||||
arr: [
|
||||
@ -33,7 +44,7 @@ describe('MilestonesService', function () {
|
||||
|
||||
describe('ARR Milestones', function () {
|
||||
it('Adds first ARR milestone and sends email', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
@ -61,10 +72,11 @@ describe('MilestonesService', function () {
|
||||
assert(arrResult.value === 1000);
|
||||
assert(arrResult.emailSentAt !== null);
|
||||
assert(arrResult.name === 'arr-1000-usd');
|
||||
assert(domainEventsSpy.calledOnce === true);
|
||||
});
|
||||
|
||||
it('Adds next ARR milestone and sends email', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestoneOne = await Milestone.create({
|
||||
type: 'arr',
|
||||
@ -92,6 +104,8 @@ describe('MilestonesService', function () {
|
||||
await repository.save(milestoneTwo);
|
||||
await repository.save(milestoneThree);
|
||||
|
||||
assert(domainEventsSpy.callCount === 3);
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
mailer: {
|
||||
@ -119,10 +133,11 @@ describe('MilestonesService', function () {
|
||||
assert(arrResult.value === 10000);
|
||||
assert(arrResult.emailSentAt !== null);
|
||||
assert(arrResult.name === 'arr-10000-usd');
|
||||
assert(domainEventsSpy.callCount === 4); // we have just created a new milestone
|
||||
});
|
||||
|
||||
it('Does not add ARR milestone for out of scope currency', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
@ -146,10 +161,11 @@ describe('MilestonesService', function () {
|
||||
|
||||
const arrResult = await milestoneEmailService.checkMilestones('arr');
|
||||
assert(arrResult === undefined);
|
||||
assert(domainEventsSpy.callCount === 0);
|
||||
});
|
||||
|
||||
it('Does not add new ARR milestone if already achieved', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestone = await Milestone.create({
|
||||
type: 'arr',
|
||||
@ -159,6 +175,8 @@ describe('MilestonesService', function () {
|
||||
|
||||
await repository.save(milestone);
|
||||
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
mailer: {
|
||||
@ -181,10 +199,11 @@ describe('MilestonesService', function () {
|
||||
|
||||
const arrResult = await milestoneEmailService.checkMilestones('arr');
|
||||
assert(arrResult === undefined);
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
});
|
||||
|
||||
it('Adds ARR milestone but does not send email if imported members are detected', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
@ -211,10 +230,11 @@ describe('MilestonesService', function () {
|
||||
assert(arrResult.currency === 'usd');
|
||||
assert(arrResult.value === 100000);
|
||||
assert(arrResult.emailSentAt === null);
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
});
|
||||
|
||||
it('Adds ARR milestone but does not send email if last email was too recent', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const lessThanTwoWeeksAgo = new Date();
|
||||
lessThanTwoWeeksAgo.setDate(lessThanTwoWeeksAgo.getDate() - 12);
|
||||
@ -227,6 +247,7 @@ describe('MilestonesService', function () {
|
||||
});
|
||||
|
||||
await repository.save(milestone);
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
@ -253,12 +274,13 @@ describe('MilestonesService', function () {
|
||||
assert(arrResult.currency === 'idr');
|
||||
assert(arrResult.value === 10000);
|
||||
assert(arrResult.emailSentAt === null);
|
||||
assert(domainEventsSpy.callCount === 2); // new milestone created
|
||||
});
|
||||
});
|
||||
|
||||
describe('Members Milestones', function () {
|
||||
it('Adds first Members milestone and sends email', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
@ -284,10 +306,11 @@ describe('MilestonesService', function () {
|
||||
assert(membersResult.type === 'members');
|
||||
assert(membersResult.value === 100);
|
||||
assert(membersResult.emailSentAt !== null);
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
});
|
||||
|
||||
it('Adds next Members milestone and sends email', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestoneOne = await Milestone.create({
|
||||
type: 'members',
|
||||
@ -314,6 +337,8 @@ describe('MilestonesService', function () {
|
||||
await repository.save(milestoneTwo);
|
||||
await repository.save(milestoneThree);
|
||||
|
||||
assert(domainEventsSpy.callCount === 3);
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
mailer: {
|
||||
@ -340,10 +365,11 @@ describe('MilestonesService', function () {
|
||||
assert(membersResult.value === 50000);
|
||||
assert(membersResult.emailSentAt !== null);
|
||||
assert(membersResult.name === 'members-50000');
|
||||
assert(domainEventsSpy.callCount === 4);
|
||||
});
|
||||
|
||||
it('Does not add new Members milestone if already achieved', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestone = await Milestone.create({
|
||||
type: 'members',
|
||||
@ -352,6 +378,8 @@ describe('MilestonesService', function () {
|
||||
|
||||
await repository.save(milestone);
|
||||
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
mailer: {
|
||||
@ -374,10 +402,11 @@ describe('MilestonesService', function () {
|
||||
|
||||
const membersResult = await milestoneEmailService.checkMilestones('members');
|
||||
assert(membersResult === undefined);
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
});
|
||||
|
||||
it('Adds Members milestone but does not send email if imported members are detected', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const milestone = await Milestone.create({
|
||||
type: 'members',
|
||||
@ -386,6 +415,8 @@ describe('MilestonesService', function () {
|
||||
|
||||
await repository.save(milestone);
|
||||
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
mailer: {
|
||||
@ -410,10 +441,11 @@ describe('MilestonesService', function () {
|
||||
assert(membersResult.type === 'members');
|
||||
assert(membersResult.value === 1000);
|
||||
assert(membersResult.emailSentAt === null);
|
||||
assert(domainEventsSpy.callCount === 2);
|
||||
});
|
||||
|
||||
it('Adds Members milestone but does not send email if last email was too recent', async function () {
|
||||
repository = new InMemoryMilestoneRepository();
|
||||
repository = new InMemoryMilestoneRepository({DomainEvents});
|
||||
|
||||
const lessThanTwoWeeksAgo = new Date();
|
||||
lessThanTwoWeeksAgo.setDate(lessThanTwoWeeksAgo.getDate() - 8);
|
||||
@ -426,6 +458,8 @@ describe('MilestonesService', function () {
|
||||
|
||||
await repository.save(milestone);
|
||||
|
||||
assert(domainEventsSpy.callCount === 1);
|
||||
|
||||
const milestoneEmailService = new MilestonesService({
|
||||
repository,
|
||||
mailer: {
|
||||
@ -450,6 +484,7 @@ describe('MilestonesService', function () {
|
||||
assert(membersResult.type === 'members');
|
||||
assert(membersResult.value === 50000);
|
||||
assert(membersResult.emailSentAt === null);
|
||||
assert(domainEventsSpy.callCount === 2);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user