mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 09:52:06 +03:00
ec841c0944
refs https://github.com/TryGhost/Team/issues/958 - The module contains a service class and not an api index as index.js file should. This rename also fixes an ESLint warning around index.js file being too complicated. - The serivice should ideally be extracted into the member repository in the future iteration
146 lines
6.0 KiB
JavaScript
146 lines
6.0 KiB
JavaScript
const moment = require('moment-timezone');
|
|
const Promise = require('bluebird');
|
|
|
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
class MembersStats {
|
|
/**
|
|
* @param {Object} config
|
|
* @param {Object} config.db - an instance holding knex connection to the database
|
|
* @param {Object} config.settingsCache - an instance of the Ghost Settings Cache
|
|
* @param {Boolean} config.isSQLite - flag identifying if storage is connected to SQLite
|
|
*/
|
|
constructor({db, settingsCache, isSQLite}) {
|
|
this._db = db;
|
|
this._settingsCache = settingsCache;
|
|
this._isSQLite = isSQLite;
|
|
}
|
|
|
|
/**
|
|
* Fetches count of all members
|
|
*/
|
|
async getTotalMembers() {
|
|
const result = await this._db.knex.raw('SELECT COUNT(id) AS total FROM members');
|
|
return this._isSQLite ? result[0].total : result[0][0].total;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Number | String} days - number of days to fetch of 'all-time' to get for all existing records
|
|
* @param {Number} totalMembers - number of registered members
|
|
* @param {String} siteTimezone - site's current timezone
|
|
*/
|
|
async getTotalMembersInRange({days, totalMembers, siteTimezone}) {
|
|
if (days === 'all-time') {
|
|
return totalMembers;
|
|
}
|
|
|
|
const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
|
|
const result = await this._db.knex.raw('SELECT COUNT(id) AS total FROM members WHERE created_at >= ?', [startOfRange]);
|
|
return this._isSQLite ? result[0].total : result[0][0].total;
|
|
}
|
|
|
|
/**
|
|
* Fetches member signups for current day
|
|
*
|
|
* @param {String} siteTimezone - site's current timezone
|
|
*/
|
|
async getNewMembersToday({siteTimezone}) {
|
|
const startOfToday = moment.tz(siteTimezone).startOf('day').utc().format(dateFormat);
|
|
const result = await this._db.knex.raw('SELECT count(id) AS total FROM members WHERE created_at >= ?', [startOfToday]);
|
|
return this._isSQLite ? result[0].total : result[0][0].total;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Number | String} days - number of days to fetch of 'all-time' to get for all existing records
|
|
* @param {Number} totalMembers - number of registered members
|
|
* @param {String} siteTimezone - site's current timezone
|
|
*/
|
|
async getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}) {
|
|
const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
|
|
const tzOffsetMins = moment.tz(siteTimezone).utcOffset();
|
|
|
|
let result;
|
|
|
|
if (this._isSQLite) {
|
|
const dateModifier = `${Math.sign(tzOffsetMins) === -1 ? '' : '+'}${tzOffsetMins} minutes`;
|
|
|
|
result = await this._db.knex('members')
|
|
.select(this._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 this._db.knex('members')
|
|
.select(this._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;
|
|
}
|
|
|
|
/**
|
|
* Fetches member's signup statistics
|
|
*
|
|
* @param {Number | String} days - number of days to fetch of 'all-time' to get for all existing records
|
|
*/
|
|
async fetch(days) {
|
|
const siteTimezone = this._settingsCache.get('timezone');
|
|
const totalMembers = await this.getTotalMembers();
|
|
|
|
// perform final calculations in parallel
|
|
const results = await Promise.props({
|
|
total: totalMembers,
|
|
total_in_range: this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
|
|
total_on_date: this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
|
|
new_today: this.getNewMembersToday({siteTimezone})
|
|
});
|
|
|
|
return results;
|
|
}
|
|
}
|
|
|
|
module.exports = MembersStats;
|