Files
habitica/migrations/tasks/habits-one-history-entry-per-day-users.js
2019-10-15 15:32:53 +02:00

163 lines
4.7 KiB
JavaScript

/* eslint-disable import/no-commonjs */
const migrationName = 'habits-one-history-entry-per-day';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Iterates over all habits and condense multiple history entries for the same day into a single one
*/
const monk = require('monk'); // eslint-disable-line import/no-extraneous-dependencies
const _ = require('lodash');
const moment = require('moment');
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const dbTasks = monk(connectionString).get('tasks', { castIds: false });
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
const query = {
migration: { $ne: migrationName },
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: { _id: 1 },
limit: 50, // just 50 users per time since we have to process all their habits as well
fields: ['_id', 'preferences.timezoneOffset', 'preferences.dayStart'],
})
.then(updateUsers)
.catch(err => {
console.log(err);
return exiting(1, `ERROR! ${err}`);
});
}
const progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users and their tasks found and modified.');
displayData();
return null;
}
const usersPromises = users.map(updateUser);
const lastUser = users[users.length - 1];
return Promise.all(usersPromises)
.then(() => processUsers(lastUser._id));
}
function updateHabit (habit, timezoneOffset, dayStart) {
if (habit && habit.history && habit.history.length > 0) {
// First remove missing entries
habit.history = habit.history.filter(entry => Boolean(entry));
habit.history = _.chain(habit.history)
// processes all entries to identify an up or down score
.forEach((entry, index) => {
if (index === 0) { // first entry doesn't have a previous one
// first value < 0 identifies a negative score as the first action
entry.scoreDirection = entry.value >= 0 ? 'up' : 'down';
} else {
// could be missing if the previous entry was null and thus excluded
const previousEntry = habit.history[index - 1];
const previousValue = previousEntry.value;
entry.scoreDirection = entry.value > previousValue ? 'up' : 'down';
}
})
.groupBy(entry => { // group entries by aggregateBy
const entryDate = moment(entry.date).zone(timezoneOffset || 0);
if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day');
return entryDate.format('YYYYMMDD');
})
.toPairs() // [key, entry]
.sortBy(([key]) => key) // sort by date
.map(keyEntryPair => {
const entries = keyEntryPair[1]; // 1 is entry, 0 is key
let scoredUp = 0;
let scoredDown = 0;
entries.forEach(entry => {
if (entry.scoreDirection === 'up') {
scoredUp += 1;
} else {
scoredDown += 1;
}
// delete the unnecessary scoreDirection and scoreNotes prop
delete entry.scoreDirection;
delete entry.scoreNotes;
});
return {
date: Number(entries[entries.length - 1].date), // keep last value
value: entries[entries.length - 1].value, // keep last value,
scoredUp,
scoredDown,
};
})
.value();
return dbTasks.update({ _id: habit._id }, {
$set: { history: habit.history },
});
}
return null;
}
function updateUser (user) {
count += 1;
const { timezoneOffset } = user.preferences;
const { dayStart } = user.preferences;
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (user._id === authorUuid) console.warn(`${authorName} being processed`);
return dbTasks.find({
type: 'habit',
userId: user._id,
})
.then(habits => Promise.all(habits.map(habit => updateHabit(habit, timezoneOffset, dayStart))))
.then(() => dbUsers.update({ _id: user._id }, {
$set: { migration: migrationName },
}))
.catch(err => {
console.log(err);
return exiting(1, `ERROR! ${err}`);
});
}
function displayData () {
console.warn(`\n${count} tasks processed\n`);
return exiting(0);
}
function exiting (code, msg) {
// 0 = success
code = code || 0; // eslint-disable-line no-param-reassign
if (code && !msg) {
msg = 'ERROR!'; // eslint-disable-line no-param-reassign
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
export default processUsers;