0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added member partitioner based on segment

refs https://github.com/TryGhost/Team/issues/828

- Before sending out batches with members we need to partition all members based on the segment they belong to. Special segment "unsegmented" is used in case none of the segments used in the emal cards cover part of the members set (for example only free members card used when emailing all members)
This commit is contained in:
Naz 2021-07-01 20:52:55 +04:00
parent 1b0aa0abd8
commit 986a7526f5
2 changed files with 132 additions and 1 deletions

View file

@ -1,6 +1,7 @@
const _ = require('lodash');
const Promise = require('bluebird');
const debug = require('@tryghost/debug')('mega');
const tpl = require('@tryghost/tpl');
const url = require('url');
const moment = require('moment');
const ObjectID = require('bson-objectid');
@ -18,6 +19,10 @@ const models = require('../../models');
const postEmailSerializer = require('./post-email-serializer');
const {getSegmentsFromHtml} = require('./segment-parser');
const messages = {
invalidSegment: 'Invalid segment value. Use one of the valid:"status:free" or "status:-free" values.'
};
const getFromAddress = () => {
let fromAddress = membersService.config.getEmailFromAddress();
@ -358,6 +363,43 @@ async function getEmailMemberRows({emailModel, memberSegment, options}) {
return memberRows;
}
/**
* Partitions array of member records according to the segment they belong to
*
* @param {Object[]} memberRows raw member rows to partition
* @param {string[]} segments segment filters to partition batches by
*
* @returns {Object} partitioned memberRows with keys that correspond segment names
*/
function partitionMembersBySegment(memberRows, segments) {
const partitions = {};
for (const memberSegment of segments) {
let segmentedMemberRows;
// NOTE: because we only support two types of segments at the moment the logic was kept dead simple
// in the future this segmentation should probably be substituted with NQL:
// memberRows.filter(member => nql(memberSegment).queryJSON(member));
if (memberSegment === 'status:free') {
segmentedMemberRows = memberRows.filter(member => member.status === 'free');
memberRows = memberRows.filter(member => member.status !== 'free');
} else if (memberSegment === 'status:-free') {
segmentedMemberRows = memberRows.filter(member => member.status !== 'free');
memberRows = memberRows.filter(member => member.status === 'free');
} else {
throw new errors.ValidationError(tpl(messages.invalidSegment));
}
partitions[memberSegment] = segmentedMemberRows;
}
if (memberRows.length) {
partitions.unsegmented = memberRows;
}
return partitions;
}
/**
* Detects segment filters in emailModel's html and creates separate batches per segment
*
@ -471,7 +513,8 @@ module.exports = {
addEmail,
retryFailedEmail,
sendTestEmail,
handleUnsubscribeRequest
handleUnsubscribeRequest,
partitionMembersBySegment // NOTE: only exposed for testing
};
/**

View file

@ -0,0 +1,88 @@
const should = require('should');
const errors = require('@tryghost/errors');
const {partitionMembersBySegment} = require('../../../../core/server/services/mega/mega');
describe('MEGA', function () {
describe('partitionMembersBySegment', function () {
it('partition with no segments', function () {
const members = [{
name: 'Free Rish',
status: 'free'
}, {
name: 'Free Matt',
status: 'free'
}, {
name: 'Paid Daniel',
status: 'paid'
}];
const segments = [];
const partitions = partitionMembersBySegment(members, segments);
partitions.unsegmented.length.should.equal(3);
partitions.unsegmented[0].name.should.equal('Free Rish');
});
it('partition members with single segment', function () {
const members = [{
name: 'Free Rish',
status: 'free'
}, {
name: 'Free Matt',
status: 'free'
}, {
name: 'Paid Daniel',
status: 'paid'
}];
const segments = ['status:free'];
const partitions = partitionMembersBySegment(members, segments);
should.exist(partitions['status:free']);
partitions['status:free'].length.should.equal(2);
partitions['status:free'][0].name.should.equal('Free Rish');
partitions['status:free'][1].name.should.equal('Free Matt');
should.exist(partitions.unsegmented);
partitions.unsegmented.length.should.equal(1);
partitions.unsegmented[0].name.should.equal('Paid Daniel');
});
it('partition members with two segments', function () {
const members = [{
name: 'Free Rish',
status: 'free'
}, {
name: 'Free Matt',
status: 'free'
}, {
name: 'Paid Daniel',
status: 'paid'
}];
const segments = ['status:free', 'status:-free'];
const partitions = partitionMembersBySegment(members, segments);
should.exist(partitions['status:free']);
partitions['status:free'].length.should.equal(2);
partitions['status:free'][0].name.should.equal('Free Rish');
partitions['status:free'][1].name.should.equal('Free Matt');
should.exist(partitions['status:-free']);
partitions['status:-free'].length.should.equal(1);
partitions['status:-free'][0].name.should.equal('Paid Daniel');
should.not.exist(partitions.unsegmented);
});
it('throws if unsupported segment has been used', function () {
const members = [];
const segments = ['not a valid segment'];
should.throws(() => {
partitionMembersBySegment(members, segments)
}, errors.ValidationError);
});
});
});