mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-26 18:52:37 +01:00
163 lines
4.7 KiB
JavaScript
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;
|