Compare commits

...

10 Commits

Author SHA1 Message Date
Phillip Thelen
ad8d0a3ed5 Revert "Use lean user for default getUser route"
This reverts commit 55cdd19215.
2026-02-03 15:46:25 +01:00
Phillip Thelen
ac322b1925 fix issues with task routes 2026-02-03 15:46:01 +01:00
Phillip Thelen
ff46459159 fix unit test 2026-02-03 15:20:41 +01:00
Phillip Thelen
e952a1dbaf lint fix 2026-02-03 12:48:58 +01:00
Phillip Thelen
e64428c89e optimize party seeking call 2026-02-03 11:04:24 +01:00
Phillip Thelen
b97dfdfa83 Use lean in more places 2026-02-03 11:04:24 +01:00
Phillip Thelen
e1a68cd02a optimize getting news posts 2026-02-03 11:04:24 +01:00
Phillip Thelen
5d7a3bedf7 don’t load list of mods on startup 2026-02-03 11:04:24 +01:00
Phillip Thelen
55cdd19215 Use lean user for default getUser route 2026-02-03 11:04:24 +01:00
Phillip Thelen
ce24ce3079 Optimize tag api calls 2026-02-03 11:04:24 +01:00
18 changed files with 60 additions and 54 deletions

View File

@@ -1353,6 +1353,8 @@ api.getLookingForParty = {
const seekers = await User
.find({
'auth.blocked': { $ne: true },
'flags.chatRevoked': { $ne: true },
'party.seeking': { $exists: true },
'invitations.party.id': { $exists: false },
'auth.timestamps.loggedin': {
@@ -1360,12 +1362,13 @@ api.getLookingForParty = {
},
})
// eslint-disable-next-line no-multi-str
.select('_id auth.blocked auth.local.username auth.timestamps backer contributor.level \
flags.chatRevoked flags.classSelected inbox.blocks invitations.party items.gear.costume \
.select('_id auth.local.username auth.timestamps backer contributor.level \
flags.classSelected inbox.blocks invitations.party items.gear.costume \
items.gear.equipped loginIncentives party._id preferences.background preferences.chair \
preferences.costume preferences.hair preferences.shirt preferences.size preferences.skin \
preferences.language profile.name stats.buffs stats.class stats.lvl')
.sort('-auth.timestamps.loggedin')
.lean()
.exec();
const filteredSeekers = seekers.filter(seeker => {

View File

@@ -24,7 +24,7 @@ const api = {};
api.getInboxMessages = {
method: 'GET',
url: '/inbox/messages',
middlewares: [authWithHeaders({ userFieldsToInclude: ['profile', 'contributor', 'backer', 'inbox'] })],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['profile', 'contributor', 'backer', 'inbox'] })],
async handler (req, res) {
const { user } = res.locals;
const { page } = req.query;

View File

@@ -40,7 +40,7 @@ const api = {};
api.createTag = {
method: 'POST',
url: '/tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -69,7 +69,7 @@ api.createTag = {
api.getTags = {
method: 'GET',
url: '/tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
res.respond(200, user.tags);
@@ -95,7 +95,7 @@ api.getTags = {
api.getTag = {
method: 'GET',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -133,7 +133,7 @@ api.getTag = {
api.updateTag = {
method: 'PUT',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -175,7 +175,7 @@ api.updateTag = {
api.reorderTags = {
method: 'POST',
url: '/reorder-tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -215,7 +215,7 @@ api.reorderTags = {
api.deleteTag = {
method: 'DELETE',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;

View File

@@ -388,7 +388,7 @@ api.getUserTasks = {
method: 'GET',
url: '/tasks/user',
middlewares: [authWithHeaders({
// Some fields (including _id, preferences) are always loaded (see middlewares/auth)
leanUser: true,
userFieldsToInclude: ['tasksOrder'],
})],
async handler (req, res) {
@@ -953,7 +953,7 @@ api.addChecklistItem = {
api.scoreCheckListItem = {
method: 'POST',
url: '/tasks/:taskId/checklist/:itemId/score',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['_id'] })],
async handler (req, res) {
const { user } = res.locals;

View File

@@ -406,7 +406,7 @@ api.getUserAnonymized = {
{ type: { $in: ['habit', 'daily', 'reward'] } },
],
};
const tasks = await Tasks.Task.find(query).exec();
const tasks = await Tasks.Task.find(query).lean().exec();
forEach(tasks, task => {
task.text = 'task text';

View File

@@ -22,6 +22,7 @@ api.purchaseHistory = {
let transactions = await Transaction
.find({ userId: req.params.memberId })
.sort({ createdAt: -1 })
.lean()
.exec();
if (!res.locals.user.hasPermission('userSupport')) {

View File

@@ -313,7 +313,9 @@ api.purchaseHistory = {
url: '/user/purchase-history',
async handler (req, res) {
const { user } = res.locals;
const transactions = await Transaction.find({ userId: user._id }).sort({ createdAt: -1 });
const transactions = await Transaction.find({ userId: user._id })
.sort({ createdAt: -1 })
.lean();
res.respond(200, transactions);
},
};

View File

@@ -53,7 +53,7 @@ api.exportUserHistory = {
const tasks = await Tasks.Task.find({
userId: user._id,
type: { $in: ['habit', 'daily'] },
}).exec();
}).lean().exec();
const output = [
['Task Name', 'Task ID', 'Task Type', 'Date', 'Value'],
@@ -92,7 +92,7 @@ async function _getUserDataForExport (user) {
const [tasks, messages] = await Promise.all([
Tasks.Task.find({
userId: user._id,
}).exec(),
}).lean().exec(),
inboxLib.getUserInbox(user, { asArray: false }),
]);
@@ -100,7 +100,6 @@ async function _getUserDataForExport (user) {
userData.inbox.messages = messages;
_(tasks)
.map(task => task.toJSON())
.groupBy(task => task.type)
.forEach((tasksPerType, taskType) => {
userData.tasks[`${taskType}s`] = tasksPerType;

View File

@@ -22,6 +22,7 @@ export async function sendChatPushNotifications (user, group, message, mentions,
'party._id': group._id,
_id: { $ne: user._id },
})
.lean()
.select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username')
.exec();

View File

@@ -25,13 +25,13 @@ export async function getGroupChat (group, options = {}) {
.sort('-timestamp');
if (before) {
const beforeMessage = await Chat.findOne({ _id: before }).exec();
const beforeMessage = await Chat.findOne({ _id: before }, { timestamp: 1 }).lean().exec();
if (beforeMessage) {
query = query.where('timestamp').lt(beforeMessage.timestamp);
}
}
const groupChat = await query.limit(effectiveLimit).exec();
const groupChat = await query.limit(effectiveLimit).lean().exec();
// @TODO: Concat old chat to keep continuity of chat stored on group object
const currentGroupChat = group.chat || [];

View File

@@ -22,7 +22,7 @@ async function usersMapByConversations (users) {
stats: 1,
flags: 1,
inbox: 1,
}).exec();
}).lean().exec();
for (const usr of loadedUsers) {
const loadedUserConversation = {

View File

@@ -169,7 +169,10 @@ api.subscribe = async function subscribe (user, receipt, headers, nextPaymentPro
{ 'purchased.plan.customerId': purchase.originalTransactionId },
{ 'purchased.plan.customerId': purchase.transactionId },
],
}).exec();
}, {
_id: 1,
'purchased.plan': 1,
}).lean().exec();
if (existingUsers.length > 0) {
if (purchase.originalTransactionId === purchase.transactionId) {
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);

View File

@@ -178,7 +178,7 @@ async function getTasks (req, res, options = {}) {
],
},
{ _id: 1 },
).exec();
).lean().exec();
}
if (upgradedGroups.length > 0) {
for (const upgradedGroup of upgradedGroups) {
@@ -270,7 +270,6 @@ async function getTasks (req, res, options = {}) {
remove(taskOrder, taskId => tasks.findIndex(task => task._id === taskId) === -1);
if (preLength !== taskOrder.length) {
owner.tasksOrder[key] = taskOrder;
owner.markModified('tasksOrder');
ownerDirty = true;
}
});
@@ -303,7 +302,17 @@ async function getTasks (req, res, options = {}) {
}
});
if (ownerDirty) await owner.save();
if (ownerDirty) {
let model;
if (challenge) {
model = Challenge;
} else if (group) {
model = Group;
} else {
model = User;
}
await model.updateOne({ _id: owner._id }, { tasksOrder: owner.tasksOrder }).exec();
}
// Remove empty values from the array and add any unordered task
orderedTasks = compact(orderedTasks).concat(unorderedTasks);

View File

@@ -82,7 +82,7 @@ export function setNextDue (task, user, dueDateOption) {
now = dateTaskIsDue;
}
const optionsForShouldDo = user.preferences.toObject();
const optionsForShouldDo = user.preferences;
optionsForShouldDo.now = now;
task.isDue = shared.shouldDo(dateTaskIsDue, task, optionsForShouldDo);

View File

@@ -186,7 +186,7 @@ export async function update (req, res, { isV3 = false }) {
],
}, {
_id: 1,
}).exec();
}).lean().exec();
matchingGroupsArray = _.map(matchingGroups, groupRecord => groupRecord._id);
}

View File

@@ -43,6 +43,7 @@ schema.statics.getNews = async function getNews (isAdmin, options = { page: 0 })
.sort({ publishDate: -1 })
.limit(POSTS_PER_PAGE)
.skip(POSTS_PER_PAGE * Number(page))
.lean()
.exec();
};

View File

@@ -1,6 +1,5 @@
import mongoose from 'mongoose';
import logger from '../../libs/logger';
import schema from './schema'; // eslint-disable-line import/no-cycle
import './hooks'; // eslint-disable-line import/no-cycle
@@ -19,19 +18,3 @@ export const nameFields = 'profile.name auth.local.username flags.verifiedUserna
export { schema };
export const model = mongoose.model('User', schema);
// Initially export an empty object so external requires will get
// the right object by reference when it's defined later
// Otherwise it would remain undefined if requested before the query executes
export const mods = [];
mongoose.model('User')
.find({ 'contributor.moderator': true })
.sort('-contributor.level -backer.npc profile.name')
.select('profile contributor backer')
.exec()
.then(foundMods => {
// Using push to maintain the reference to mods
mods.push(...foundMods);
})
.catch(err => logger.error(err));

View File

@@ -367,14 +367,14 @@ schema.methods.getUtcOffset = function getUtcOffset () {
return common.fns.getUtcOffset(this);
};
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
schema.statics.daysUserHasMissed = function daysUserHasMissed (user, now, req = {}) {
// If the user's timezone has changed (due to travel or daylight savings),
// cron can be triggered twice in one day, so we check for that and use
// both timezones to work out if cron should run.
// CDS = Custom Day Start time.
let timezoneUtcOffsetFromUserPrefs = this.getUtcOffset();
const timezoneUtcOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron)
? -this.preferences.timezoneOffsetAtLastCron
let timezoneUtcOffsetFromUserPrefs = common.fns.getUtcOffset(user);
const timezoneUtcOffsetAtLastCron = Number.isFinite(user.preferences.timezoneOffsetAtLastCron)
? -user.preferences.timezoneOffsetAtLastCron
: timezoneUtcOffsetFromUserPrefs;
let timezoneUtcOffsetFromBrowser = typeof req.header === 'function' && -Number(req.header('x-user-timezoneoffset'));
@@ -386,16 +386,16 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
if (timezoneUtcOffsetFromBrowser !== timezoneUtcOffsetFromUserPrefs) {
// The user's browser has just told Habitica that the user's timezone has
// changed so store and use the new zone.
this.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
user.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
timezoneUtcOffsetFromUserPrefs = timezoneUtcOffsetFromBrowser;
}
let lastCronTime = this.lastCron;
if (this.auth.timestamps.loggedIn < lastCronTime) {
lastCronTime = this.auth.timestamps.loggedIn;
let lastCronTime = user.lastCron;
if (user.auth.timestamps.loggedIn < lastCronTime) {
lastCronTime = user.auth.timestamps.loggedIn;
}
// How many days have we missed using the user's current timezone:
let daysMissed = daysSince(lastCronTime, defaults({ now }, this.preferences));
let daysMissed = daysSince(lastCronTime, defaults({ now }, user.preferences));
if (timezoneUtcOffsetAtLastCron !== timezoneUtcOffsetFromUserPrefs) {
// Give the user extra time based on the difference in timezones
@@ -410,7 +410,7 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
const daysMissedOldZone = daysSince(lastCronTime, defaults({
now,
timezoneUtcOffsetOverride: timezoneUtcOffsetAtLastCron,
}, this.preferences));
}, user.preferences));
if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) {
// The timezone change was in the unsafe direction.
@@ -447,12 +447,12 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
const timezoneOffsetDiff = timezoneUtcOffsetFromUserPrefs - timezoneUtcOffsetAtLastCron;
// e.g., for dangerous zone change: -300 - -240 = -60 or 600 - 660= -60
this.lastCron = moment(lastCronTime).subtract(timezoneOffsetDiff, 'minutes');
user.lastCron = moment(lastCronTime).subtract(timezoneOffsetDiff, 'minutes');
// NB: We don't change this.auth.timestamps.loggedin so that will still record
// the time that the previous cron actually ran.
// From now on we can ignore the old timezone:
// This is still timezoneOffset for backwards compatibility reasons.
this.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
user.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
} else {
// Both old and new timezones indicate that cron should
// NOT run.
@@ -474,6 +474,10 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
return { daysMissed, timezoneUtcOffsetFromUserPrefs };
};
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
return schema.statics.daysUserHasMissed(this, now, req);
};
async function getUserGroupData (user) {
const userGroups = user.getGroups();