Moved members stats code out of members controller

refs #12537

- `stats` method in members controller is quite big and does much more then controller method code should - few calls to relevant modules
- Extracted code "as is" into members serivce
- Next step will be to refactor this module as a class pattern with DI parameters
This commit is contained in:
Naz 2021-01-13 16:30:24 +13:00
parent 91633faf24
commit e62c4075f0
3 changed files with 114 additions and 100 deletions

View File

@ -10,7 +10,6 @@ const membersService = require('../../services/members');
const jobsService = require('../../services/jobs');
const settingsCache = require('../../services/settings/cache');
const {i18n} = require('../../lib/common');
const db = require('../../data/db');
const ghostMailer = new GhostMailer();
const allowedIncludes = ['email_recipients'];
@ -418,108 +417,15 @@ module.exports = {
}
},
async query(frame) {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const isSQLite = config.get('database:client') === 'sqlite3';
const siteTimezone = settingsCache.get('timezone');
const tzOffsetMins = moment.tz(siteTimezone).utcOffset();
const days = frame.options.days === 'all-time' ? 'all-time' : Number(frame.options.days || 30);
const isSQLite = config.get('database:client') === 'sqlite3';
// get total members before other stats because the figure is used multiple times
async function getTotalMembers() {
const result = await db.knex.raw('SELECT COUNT(id) AS total FROM members');
return isSQLite ? result[0].total : result[0][0].total;
}
const totalMembers = await getTotalMembers();
async function getTotalMembersInRange() {
if (days === 'all-time') {
return totalMembers;
}
const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
const result = await db.knex.raw('SELECT COUNT(id) AS total FROM members WHERE created_at >= ?', [startOfRange]);
return isSQLite ? result[0].total : result[0][0].total;
}
async function getTotalMembersOnDatesInRange() {
const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
let result;
if (isSQLite) {
const dateModifier = `${Math.sign(tzOffsetMins) === -1 ? '' : '+'}${tzOffsetMins} minutes`;
result = await db.knex('members')
.select(db.knex.raw('DATE(created_at, ?) AS created_at, COUNT(DATE(created_at, ?)) AS count', [dateModifier, dateModifier]))
.where((builder) => {
if (days !== 'all-time') {
builder.whereRaw('created_at >= ?', [startOfRange]);
}
}).groupByRaw('DATE(created_at, ?)', [dateModifier]);
} else {
const mins = Math.abs(tzOffsetMins) % 60;
const hours = (Math.abs(tzOffsetMins) - mins) / 60;
const utcOffset = `${Math.sign(tzOffsetMins) === -1 ? '-' : '+'}${hours}:${mins < 10 ? '0' : ''}${mins}`;
result = await db.knex('members')
.select(db.knex.raw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?)) AS created_at, COUNT(CONVERT_TZ(created_at, \'+00:00\', ?)) AS count', [utcOffset, utcOffset]))
.where((builder) => {
if (days !== 'all-time') {
builder.whereRaw('created_at >= ?', [startOfRange]);
}
})
.groupByRaw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?))', [utcOffset]);
}
// sql doesn't return rows with a 0 count so we build an object
// with sparse results to reference by date rather than performing
// multiple finds across an array
const resultObject = {};
result.forEach((row) => {
resultObject[moment(row.created_at).format('YYYY-MM-DD')] = row.count;
});
// loop over every date in the range so we can return a contiguous range object
const totalInRange = Object.values(resultObject).reduce((acc, value) => acc + value, 0);
let runningTotal = totalMembers - totalInRange;
let currentRangeDate;
if (days === 'all-time') {
// start from the date of first created member
currentRangeDate = moment(moment(result[0].created_at).format('YYYY-MM-DD')).tz(siteTimezone);
} else {
currentRangeDate = moment.tz(siteTimezone).subtract(days - 1, 'days');
}
let endDate = moment.tz(siteTimezone).add(1, 'hour');
const output = {};
while (currentRangeDate.isBefore(endDate)) {
let dateStr = currentRangeDate.format('YYYY-MM-DD');
runningTotal += resultObject[dateStr] || 0;
output[dateStr] = runningTotal;
currentRangeDate = currentRangeDate.add(1, 'day');
}
return output;
}
async function getNewMembersToday() {
const startOfToday = moment.tz(siteTimezone).startOf('day').utc().format(dateFormat);
const result = await db.knex.raw('SELECT count(id) AS total FROM members WHERE created_at >= ?', [startOfToday]);
return isSQLite ? result[0].total : result[0][0].total;
}
// perform final calculations in parallel
const results = await Promise.props({
total: totalMembers,
total_in_range: getTotalMembersInRange(),
total_on_date: getTotalMembersOnDatesInRange(),
new_today: getNewMembersToday()
return await membersService.stats({
siteTimezone,
days,
isSQLite
});
return results;
}
}
};

View File

@ -93,7 +93,9 @@ const membersService = {
stripeConnect: require('./stripe-connect'),
importer: new MembersCSVImporter({storagePath: config.getContentPath('data')}, settingsCache, () => membersApi)
importer: new MembersCSVImporter({storagePath: config.getContentPath('data')}, settingsCache, () => membersApi),
stats: require('./stats')
};
module.exports = membersService;

View File

@ -0,0 +1,106 @@
const moment = require('moment-timezone');
const Promise = require('bluebird');
const db = require('../../../data/db');
const stats = async ({siteTimezone, days, isSQLite}) => {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const tzOffsetMins = moment.tz(siteTimezone).utcOffset();
// get total members before other stats because the figure is used multiple times
async function getTotalMembers() {
const result = await db.knex.raw('SELECT COUNT(id) AS total FROM members');
return isSQLite ? result[0].total : result[0][0].total;
}
const totalMembers = await getTotalMembers();
async function getTotalMembersInRange() {
if (days === 'all-time') {
return totalMembers;
}
const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
const result = await db.knex.raw('SELECT COUNT(id) AS total FROM members WHERE created_at >= ?', [startOfRange]);
return isSQLite ? result[0].total : result[0][0].total;
}
async function getTotalMembersOnDatesInRange() {
const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
let result;
if (isSQLite) {
const dateModifier = `${Math.sign(tzOffsetMins) === -1 ? '' : '+'}${tzOffsetMins} minutes`;
result = await db.knex('members')
.select(db.knex.raw('DATE(created_at, ?) AS created_at, COUNT(DATE(created_at, ?)) AS count', [dateModifier, dateModifier]))
.where((builder) => {
if (days !== 'all-time') {
builder.whereRaw('created_at >= ?', [startOfRange]);
}
}).groupByRaw('DATE(created_at, ?)', [dateModifier]);
} else {
const mins = Math.abs(tzOffsetMins) % 60;
const hours = (Math.abs(tzOffsetMins) - mins) / 60;
const utcOffset = `${Math.sign(tzOffsetMins) === -1 ? '-' : '+'}${hours}:${mins < 10 ? '0' : ''}${mins}`;
result = await db.knex('members')
.select(db.knex.raw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?)) AS created_at, COUNT(CONVERT_TZ(created_at, \'+00:00\', ?)) AS count', [utcOffset, utcOffset]))
.where((builder) => {
if (days !== 'all-time') {
builder.whereRaw('created_at >= ?', [startOfRange]);
}
})
.groupByRaw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?))', [utcOffset]);
}
// sql doesn't return rows with a 0 count so we build an object
// with sparse results to reference by date rather than performing
// multiple finds across an array
const resultObject = {};
result.forEach((row) => {
resultObject[moment(row.created_at).format('YYYY-MM-DD')] = row.count;
});
// loop over every date in the range so we can return a contiguous range object
const totalInRange = Object.values(resultObject).reduce((acc, value) => acc + value, 0);
let runningTotal = totalMembers - totalInRange;
let currentRangeDate;
if (days === 'all-time') {
// start from the date of first created member
currentRangeDate = moment(moment(result[0].created_at).format('YYYY-MM-DD')).tz(siteTimezone);
} else {
currentRangeDate = moment.tz(siteTimezone).subtract(days - 1, 'days');
}
let endDate = moment.tz(siteTimezone).add(1, 'hour');
const output = {};
while (currentRangeDate.isBefore(endDate)) {
let dateStr = currentRangeDate.format('YYYY-MM-DD');
runningTotal += resultObject[dateStr] || 0;
output[dateStr] = runningTotal;
currentRangeDate = currentRangeDate.add(1, 'day');
}
return output;
}
async function getNewMembersToday() {
const startOfToday = moment.tz(siteTimezone).startOf('day').utc().format(dateFormat);
const result = await db.knex.raw('SELECT count(id) AS total FROM members WHERE created_at >= ?', [startOfToday]);
return isSQLite ? result[0].total : result[0][0].total;
}
// perform final calculations in parallel
const results = await Promise.props({
total: totalMembers,
total_in_range: getTotalMembersInRange(),
total_on_date: getTotalMembersOnDatesInRange(),
new_today: getNewMembersToday()
});
return results;
};
module.exports = stats;