perf: optimize friend queries (@fehmer) (#7080)

Combine two queries (first get all friend UIDs, then call leaderboard)
into one query to reduce db roundtrips.

Use the same approach for the friends list in user dal.

Note: when updating mongodb to 6+ we could use unionWith in case we
don't need the metadata (lb use-case)
This commit is contained in:
Christian Fehmer 2025-11-13 15:31:55 +01:00 committed by GitHub
parent 81f09b9b90
commit 949e2baa48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 449 additions and 285 deletions

View file

@ -11,6 +11,7 @@ import { ObjectId } from "mongodb";
import * as ConnectionsDal from "../../../src/dal/connections";
import { createConnection } from "../../__testData__/connections";
import { createUser } from "../../__testData__/users";
describe("ConnectionsDal", () => {
beforeAll(async () => {
@ -401,4 +402,92 @@ describe("ConnectionsDal", () => {
]);
});
});
describe("aggregateWithAcceptedConnections", () => {
it("should return friend uids", async () => {
//GIVE
const uid = (await createUser()).uid;
const friendOne = await createConnection({
initiatorUid: uid,
receiverUid: (await createUser()).uid,
status: "accepted",
});
const friendTwo = await createConnection({
initiatorUid: (await createUser()).uid,
receiverUid: uid,
status: "accepted",
});
const friendThree = await createConnection({
initiatorUid: (await createUser()).uid,
receiverUid: uid,
status: "accepted",
});
const _pending = await createConnection({
initiatorUid: uid,
receiverUid: (await createUser()).uid,
status: "pending",
});
const _blocked = await createConnection({
initiatorUid: uid,
receiverUid: (await createUser()).uid,
status: "blocked",
});
const _decoy = await createConnection({
receiverUid: (await createUser()).uid,
status: "accepted",
});
//WHEN
const friendUids = await ConnectionsDal.aggregateWithAcceptedConnections<{
uid: string;
}>({ collectionName: "users", uid }, [{ $project: { uid: true } }]);
//THEN
expect(friendUids.flatMap((it) => it.uid).toSorted()).toEqual([
uid,
friendOne.receiverUid,
friendTwo.initiatorUid,
friendThree.initiatorUid,
]);
});
it("should return friend uids and metaData", async () => {
//GIVE
const me = await createUser();
const friend = await createUser();
const connection = await createConnection({
initiatorUid: me.uid,
receiverUid: friend.uid,
status: "accepted",
});
//WHEN
const friendUids = await ConnectionsDal.aggregateWithAcceptedConnections(
{ collectionName: "users", uid: me.uid, includeMetaData: true },
[
{
$project: {
uid: true,
lastModified: "$connectionMeta.lastModified",
connectionId: "$connectionMeta._id",
},
},
]
);
//THEN
expect(friendUids).toEqual([
{
_id: friend._id,
connectionId: connection._id,
lastModified: connection.lastModified,
uid: friend.uid,
},
{
_id: me._id,
uid: me.uid,
},
]);
});
});
});

View file

@ -1,4 +1,4 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { describe, it, expect, vi, afterEach } from "vitest";
import _ from "lodash";
import { ObjectId } from "mongodb";
import * as UserDal from "../../../src/dal/user";
@ -11,6 +11,7 @@ import * as DB from "../../../src/init/db";
import { LbPersonalBests } from "../../../src/utils/pb";
import { pb } from "../../__testData__/users";
import { createConnection } from "../../__testData__/connections";
describe("LeaderboardsDal", () => {
afterEach(async () => {
@ -307,9 +308,20 @@ describe("LeaderboardsDal", () => {
it("should get for friends only", async () => {
//GIVEN
const rank1 = await createUser(lbBests(pb(90), pb(100, 90, 2)));
const uid = rank1.uid;
const _rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
const _rank3 = await createUser(lbBests(undefined, pb(100, 80, 2)));
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
//two friends, one is not on the leaderboard
await createConnection({
initiatorUid: uid,
receiverUid: rank4.uid,
status: "accepted",
});
await createConnection({ initiatorUid: uid, status: "accepted" });
await LeaderboardsDal.update("time", "60", "english");
//WHEN
@ -321,7 +333,7 @@ describe("LeaderboardsDal", () => {
0,
50,
false,
[rank1.uid, rank4.uid]
uid
)) as LeaderboardsDal.DBLeaderboardEntry[];
//THEN
@ -335,11 +347,23 @@ describe("LeaderboardsDal", () => {
it("should get for friends only with page", async () => {
//GIVEN
const rank1 = await createUser(lbBests(pb(90), pb(105, 90, 2)));
const uid = rank1.uid;
const rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
const _rank3 = await createUser(lbBests(undefined, pb(95, 80, 2)));
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
await LeaderboardsDal.update("time", "60", "english");
await createConnection({
initiatorUid: uid,
receiverUid: rank2.uid,
status: "accepted",
});
await createConnection({
initiatorUid: rank4.uid,
receiverUid: uid,
status: "accepted",
});
//WHEN
const results = (await LeaderboardsDal.get(
"time",
@ -348,7 +372,7 @@ describe("LeaderboardsDal", () => {
1,
2,
false,
[rank1.uid, rank2.uid, rank4.uid]
uid
)) as LeaderboardsDal.DBLeaderboardEntry[];
//THEN
@ -360,6 +384,7 @@ describe("LeaderboardsDal", () => {
});
it("should return empty list if no friends", async () => {
//GIVEN
const uid = new ObjectId().toHexString();
//WHEN
const results = (await LeaderboardsDal.get(
@ -369,7 +394,7 @@ describe("LeaderboardsDal", () => {
1,
2,
false,
[]
uid
)) as LeaderboardsDal.DBLeaderboardEntry[];
//THEN
expect(results).toEqual([]);
@ -378,10 +403,10 @@ describe("LeaderboardsDal", () => {
describe("getCount / getRank", () => {
it("should get count", async () => {
//GIVEN
await createUser(lbBests(undefined, pb(105)));
await createUser(lbBests(undefined, pb(100)));
const me = await createUser(lbBests(undefined, pb(95)));
await createUser(lbBests(undefined, pb(90)));
await createUser(lbBests(undefined, pb(105)), { name: "One" });
await createUser(lbBests(undefined, pb(100)), { name: "Two" });
const me = await createUser(lbBests(undefined, pb(95)), { name: "Me" });
await createUser(lbBests(undefined, pb(90)), { name: "Three" });
await LeaderboardsDal.update("time", "60", "english");
//WHEN / THEN
@ -405,19 +430,26 @@ describe("LeaderboardsDal", () => {
await createUser(lbBests(undefined, pb(95)));
const friendTwo = await createUser(lbBests(undefined, pb(90)));
const me = await createUser(lbBests(undefined, pb(99)));
console.log("me", me.uid);
await LeaderboardsDal.update("time", "60", "english");
const friends = [friendOne.uid, friendTwo.uid, me.uid];
await createConnection({
initiatorUid: me.uid,
receiverUid: friendOne.uid,
status: "accepted",
});
await createConnection({
initiatorUid: friendTwo.uid,
receiverUid: me.uid,
status: "accepted",
});
//WHEN / THEN
expect(await LeaderboardsDal.getCount("time", "60", "english", friends)) //
expect(await LeaderboardsDal.getCount("time", "60", "english", me.uid)) //
.toEqual(3);
expect(
await LeaderboardsDal.getRank("time", "60", "english", me.uid, friends)
await LeaderboardsDal.getRank("time", "60", "english", me.uid, true)
) //
.toEqual(
expect.objectContaining({

View file

@ -16,9 +16,8 @@ export async function createConnection(
},
maxPerUser
);
await ConnectionsDal.getCollection().updateOne(
{ _id: result._id },
{ $set: data }
);
await ConnectionsDal.__testing
.getCollection()
.updateOne({ _id: result._id }, { $set: data });
return { ...result, ...data };
}

View file

@ -30,12 +30,10 @@ describe("Loaderboard Controller", () => {
describe("get leaderboard", () => {
const getLeaderboardMock = vi.spyOn(LeaderboardDal, "get");
const getLeaderboardCountMock = vi.spyOn(LeaderboardDal, "getCount");
const getFriendsUidsMock = vi.spyOn(ConnectionsDal, "getFriendsUids");
beforeEach(() => {
getLeaderboardMock.mockClear();
getLeaderboardCountMock.mockClear();
getFriendsUidsMock.mockClear();
getLeaderboardCountMock.mockResolvedValue(42);
});
@ -154,7 +152,6 @@ describe("Loaderboard Controller", () => {
//GIVEN
await enableConnectionsFeature(true);
getLeaderboardMock.mockResolvedValue([]);
getFriendsUidsMock.mockResolvedValue(["uidOne", "uidTwo"]);
getLeaderboardCountMock.mockResolvedValue(2);
//WHEN
@ -180,13 +177,13 @@ describe("Loaderboard Controller", () => {
0,
50,
false,
["uidOne", "uidTwo"]
uid
);
expect(getLeaderboardCountMock).toHaveBeenCalledWith(
"time",
"60",
"english",
["uidOne", "uidTwo"]
uid
);
});
@ -286,11 +283,9 @@ describe("Loaderboard Controller", () => {
describe("get rank", () => {
const getLeaderboardRankMock = vi.spyOn(LeaderboardDal, "getRank");
const getFriendsUidsMock = vi.spyOn(ConnectionsDal, "getFriendsUids");
afterEach(() => {
getLeaderboardRankMock.mockClear();
getFriendsUidsMock.mockClear();
});
it("fails withouth authentication", async () => {
@ -335,14 +330,12 @@ describe("Loaderboard Controller", () => {
"60",
"english",
uid,
undefined
false
);
});
it("should get for english time 60 friends only", async () => {
//GIVEN
await enableConnectionsFeature(true);
const friends = ["friendOne", "friendTwo"];
getFriendsUidsMock.mockResolvedValue(friends);
getLeaderboardRankMock.mockResolvedValue({} as any);
//WHEN
@ -363,9 +356,8 @@ describe("Loaderboard Controller", () => {
"60",
"english",
uid,
friends
true
);
expect(getFriendsUidsMock).toHaveBeenCalledWith(uid);
});
it("should get with ape key", async () => {
await acceptApeKeys(true);

View file

@ -43,11 +43,7 @@ export async function getLeaderboard(
throw new MonkeyError(404, "There is no leaderboard for this mode");
}
const friendUids = await getFriendsUids(
uid,
friendsOnly === true,
connectionsConfig
);
const friendsOnlyUid = getFriendsOnlyUid(uid, friendsOnly, connectionsConfig);
const leaderboard = await LeaderboardsDAL.get(
mode,
@ -56,7 +52,7 @@ export async function getLeaderboard(
page,
pageSize,
req.ctx.configuration.users.premium.enabled,
friendUids
friendsOnlyUid
);
if (leaderboard === false) {
@ -70,7 +66,7 @@ export async function getLeaderboard(
mode,
mode2,
language,
friendUids
friendsOnlyUid
);
const normalizedLeaderboard = leaderboard.map((it) => _.omit(it, ["_id"]));
@ -88,18 +84,12 @@ export async function getRankFromLeaderboard(
const { uid } = req.ctx.decodedToken;
const connectionsConfig = req.ctx.configuration.connections;
const friendUids = await getFriendsUids(
uid,
friendsOnly === true,
connectionsConfig
);
const data = await LeaderboardsDAL.getRank(
mode,
mode2,
language,
uid,
friendUids
getFriendsOnlyUid(uid, friendsOnly, connectionsConfig) !== undefined
);
if (data === false) {
throw new MonkeyError(
@ -284,3 +274,17 @@ async function getFriendsUids(
}
return undefined;
}
function getFriendsOnlyUid(
uid: string,
friendsOnly: boolean | undefined,
friendsConfig: Configuration["connections"]
): string | undefined {
if (uid !== "" && friendsOnly === true) {
if (!friendsConfig.enabled) {
throw new MonkeyError(503, "This feature is currently unavailable.");
}
return uid;
}
return undefined;
}

View file

@ -1,4 +1,4 @@
import { Collection, Filter, ObjectId } from "mongodb";
import { Collection, Document, Filter, ObjectId } from "mongodb";
import * as db from "../init/db";
import { Connection, ConnectionStatus } from "@monkeytype/schemas/connections";
import MonkeyError from "../utils/error";
@ -10,7 +10,7 @@ export type DBConnection = WithObjectId<
}
>;
export const getCollection = (): Collection<DBConnection> =>
const getCollection = (): Collection<DBConnection> =>
db.collection("connections");
export async function getConnections(options: {
@ -209,6 +209,125 @@ export async function getFriendsUids(uid: string): Promise<string[]> {
);
}
/**
* aggregate the given `pipeline` on the `collectionName` for each friendUid and the given `uid`.
* @param pipeline
* @param options
* @returns
*/
export async function aggregateWithAcceptedConnections<T>(
options: {
uid: string;
/**
* target collection
*/
collectionName: string;
/**
* uid field on the collection, defaults to `uid`
*/
uidField?: string;
/**
* add meta data `connectionMeta.lastModified` and *connectionMeta._id` to the document
*/
includeMetaData?: boolean;
},
pipeline: Document[]
): Promise<T[]> {
const metaData = options.includeMetaData
? {
let: {
lastModified: "$lastModified",
connectionId: "$connectionId",
},
pipeline: [
{
$addFields: {
"connectionMeta.lastModified": "$$lastModified",
"connectionMeta._id": "$$connectionId",
},
},
],
}
: {};
const { uid, collectionName, uidField } = options;
const fullPipeline = [
{
$match: {
status: "accepted",
//uid is friend or initiator
$or: [{ initiatorUid: uid }, { receiverUid: uid }],
},
},
{
$project: {
lastModified: true,
uid: {
//pick the other user, not uid
$cond: {
if: { $eq: ["$receiverUid", uid] },
// oxlint-disable-next-line no-thenable
then: "$initiatorUid",
else: "$receiverUid",
},
},
},
},
// we want to fetch the data for our uid as well, add it to the list of documents
// workaround for missing unionWith + $documents in mongodb 5.0
{
$group: {
_id: null,
data: {
$push: {
uid: "$uid",
lastModified: "$lastModified",
connectionId: "$_id",
},
},
},
},
{
$project: {
data: {
$concatArrays: ["$data", [{ uid }]],
},
},
},
{ $unwind: "$data" },
{ $replaceRoot: { newRoot: "$data" } },
/* end of workaround, this is the replacement for >= 5.1
{ $addFields: { connectionId: "$_id" } },
{ $project: { uid: true, lastModified: true, connectionId: true } },
{
$unionWith: {
pipeline: [{ $documents: [{ uid }] }],
},
},
*/
{
//replace with $unionWith in MongoDB 6 or newer
$lookup: {
from: collectionName,
localField: "uid",
foreignField: uidField ?? "uid",
as: "result",
...metaData,
},
},
{ $match: { result: { $ne: [] } } },
{ $replaceRoot: { newRoot: { $first: "$result" } } },
...pipeline,
];
//console.log(JSON.stringify(fullPipeline, null, 4));
return (await getCollection().aggregate(fullPipeline).toArray()) as T[];
}
function getKey(initiatorUid: string, receiverUid: string): string {
const ids = [initiatorUid, receiverUid];
ids.sort();
@ -223,3 +342,7 @@ export async function createIndicies(): Promise<void> {
//make sure there is only one connection for each initiatorr/receiver
await getCollection().createIndex({ key: 1 }, { unique: true });
}
export const __testing = {
getCollection,
};

View file

@ -14,19 +14,25 @@ import { LeaderboardEntry } from "@monkeytype/schemas/leaderboards";
import { omit } from "lodash";
import { DBUser, getUsersCollection } from "./user";
import MonkeyError from "../utils/error";
import { aggregateWithAcceptedConnections } from "./connections";
export type DBLeaderboardEntry = LeaderboardEntry & {
_id: ObjectId;
};
function getCollectionName(key: {
language: string;
mode: string;
mode2: string;
}): string {
return `leaderboards.${key.language}.${key.mode}.${key.mode2}`;
}
export const getCollection = (key: {
language: string;
mode: string;
mode2: string;
}): Collection<DBLeaderboardEntry> =>
db.collection<DBLeaderboardEntry>(
`leaderboards.${key.language}.${key.mode}.${key.mode2}`
);
db.collection<DBLeaderboardEntry>(getCollectionName(key));
export async function get(
mode: string,
@ -35,42 +41,45 @@ export async function get(
page: number,
pageSize: number,
premiumFeaturesEnabled: boolean = false,
userIds?: string[]
uid?: string
): Promise<DBLeaderboardEntry[] | false> {
if (page < 0 || pageSize < 0) {
throw new MonkeyError(500, "Invalid page or pageSize");
}
if (userIds?.length === 0) {
return [];
}
const skip = page * pageSize;
const limit = pageSize;
let leaderboard: DBLeaderboardEntry[] | false = [];
const pipeline: Document[] = [
{ $sort: { rank: 1 } },
{ $skip: skip },
{ $limit: limit },
];
if (userIds !== undefined) {
pipeline.unshift(
{ $match: { uid: { $in: userIds } } },
{
$setWindowFields: {
sortBy: { rank: 1 },
output: { friendsRank: { $documentNumber: {} } },
},
}
);
}
try {
let leaderboard = (await getCollection({ language, mode, mode2 })
.aggregate(pipeline)
.toArray()) as DBLeaderboardEntry[];
if (uid !== undefined) {
leaderboard = await aggregateWithAcceptedConnections(
{
uid,
collectionName: getCollectionName({ language, mode, mode2 }),
},
[
{
$setWindowFields: {
sortBy: { rank: 1 },
output: { friendsRank: { $documentNumber: {} } },
},
},
...pipeline,
]
);
} else {
leaderboard = await getCollection({ language, mode, mode2 })
.aggregate<DBLeaderboardEntry>(pipeline)
.toArray();
}
if (!premiumFeaturesEnabled) {
leaderboard = leaderboard.map((it) => omit(it, "isPremium"));
}
@ -92,23 +101,30 @@ export async function getCount(
mode: string,
mode2: string,
language: string,
userIds?: string[]
uid?: string
): Promise<number> {
const key = `${language}_${mode}_${mode2}`;
if (userIds === undefined && cachedCounts.has(key)) {
if (uid === undefined && cachedCounts.has(key)) {
return cachedCounts.get(key) as number;
} else {
const lb = getCollection({
language,
mode,
mode2,
});
if (userIds === undefined) {
const count = await lb.estimatedDocumentCount();
if (uid === undefined) {
const count = await getCollection({
language,
mode,
mode2,
}).estimatedDocumentCount();
cachedCounts.set(key, count);
return count;
} else {
return lb.countDocuments({ uid: { $in: userIds } });
return (
await aggregateWithAcceptedConnections(
{
collectionName: getCollectionName({ language, mode, mode2 }),
uid,
},
[{ $project: { _id: true } }]
)
).length;
}
}
}
@ -118,32 +134,33 @@ export async function getRank(
mode2: string,
language: string,
uid: string,
userIds?: string[]
friendsOnly: boolean = false
): Promise<LeaderboardEntry | null | false> {
try {
if (userIds === undefined) {
if (!friendsOnly) {
const entry = await getCollection({ language, mode, mode2 }).findOne({
uid,
});
return entry;
} else if (userIds.length === 0) {
return null;
} else {
const entry = await getCollection({ language, mode, mode2 })
.aggregate([
{ $match: { uid: { $in: userIds } } },
const results =
await aggregateWithAcceptedConnections<DBLeaderboardEntry>(
{
$setWindowFields: {
sortBy: { rank: 1 },
output: { friendsRank: { $documentNumber: {} } },
},
collectionName: getCollectionName({ language, mode, mode2 }),
uid,
},
{ $match: { uid } },
])
.toArray();
return entry[0] as DBLeaderboardEntry;
[
{
$setWindowFields: {
sortBy: { rank: 1 },
output: { friendsRank: { $documentNumber: {} } },
},
},
{ $match: { uid } },
]
);
return results[0] ?? null;
}
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@ -164,7 +181,7 @@ export async function update(
rank?: number;
}> {
const key = `lbPersonalBests.${mode}.${mode2}.${language}`;
const lbCollectionName = `leaderboards.${language}.${mode}.${mode2}`;
const lbCollectionName = getCollectionName({ language, mode, mode2 });
const minTimeTyping = (await getCachedConfiguration(true)).leaderboards
.minTimeTyping;
const lb = db.collection<DBUser>("users").aggregate<LeaderboardEntry>(

View file

@ -34,7 +34,7 @@ import { Result as ResultType } from "@monkeytype/schemas/results";
import { Configuration } from "@monkeytype/schemas/configuration";
import { isToday, isYesterday } from "@monkeytype/util/date-and-time";
import GeorgeQueue from "../queues/george-queue";
import { getCollection as getConnectionCollection } from "./connections";
import { aggregateWithAcceptedConnections } from "./connections";
export type DBUserTag = WithObjectId<UserTag>;
@ -1228,210 +1228,118 @@ async function updateUser(
}
export async function getFriends(uid: string): Promise<DBFriend[]> {
return (await getConnectionCollection()
.aggregate([
{
$match: {
//uid is friend or initiator
$and: [
{
$or: [{ initiatorUid: uid }, { receiverUid: uid }],
status: "accepted",
},
],
},
},
return await aggregateWithAcceptedConnections(
{
uid,
collectionName: "users",
includeMetaData: true,
},
[
{
$project: {
receiverUid: true,
initiatorUid: true,
lastModified: true,
_id: false,
uid: true,
connectionId: "$connectionMeta._id",
lastModified: "$connectionMeta.lastModified",
name: true,
discordId: true,
discordAvatar: true,
startedTests: true,
completedTests: true,
timeTyping: true,
xp: true,
"streak.length": true,
"streak.maxLength": true,
personalBests: true,
"inventory.badges": true,
"premium.expirationTimestamp": true,
banned: 1,
lbOptOut: 1,
},
},
{
$addFields: {
//pick the other user, not uid
uid: {
top15: {
$reduce: {
//find highest wpm from time 15 PBs
input: "$personalBests.time.15",
initialValue: {},
in: {
$cond: [
{ $gte: ["$$this.wpm", "$$value.wpm"] },
"$$this",
"$$value",
],
},
},
},
top60: {
$reduce: {
//find highest wpm from time 60 PBs
input: "$personalBests.time.60",
initialValue: {},
in: {
$cond: [
{ $gte: ["$$this.wpm", "$$value.wpm"] },
"$$this",
"$$value",
],
},
},
},
badgeId: {
$ifNull: [
{
$first: {
$map: {
input: {
$filter: {
input: "$inventory.badges",
as: "badge",
cond: { $eq: ["$$badge.selected", true] },
},
},
as: "selectedBadge",
in: "$$selectedBadge.id",
},
},
},
"$$REMOVE",
],
},
isPremium: {
$cond: {
if: { $eq: ["$receiverUid", uid] },
if: {
$or: [
{ $eq: ["$premium.expirationTimestamp", -1] },
{
$gt: ["$premium.expirationTimestamp", { $toLong: "$$NOW" }],
},
],
},
// oxlint-disable-next-line no-thenable
then: "$initiatorUid",
else: "$receiverUid",
then: true,
else: "$$REMOVE",
},
},
},
},
// we want to fetch the data for our uid as well, add it to the list of documents
// workaround for missing unionWith + $documents in mongodb 5.0
{
$group: {
_id: null,
data: {
$push: {
uid: "$uid",
lastModified: "$lastModified",
connectionId: "$_id",
},
},
$addFields: {
//remove nulls
top15: { $ifNull: ["$top15", "$$REMOVE"] },
top60: { $ifNull: ["$top60", "$$REMOVE"] },
badgeId: { $ifNull: ["$badgeId", "$$REMOVE"] },
lastModified: "$lastModified",
},
},
{
$project: {
data: {
$concatArrays: ["$data", [{ uid }]],
},
personalBests: false,
inventory: false,
premium: false,
},
},
{
$unwind: "$data",
},
/* end of workaround, this is the replacement for >= 5.1
{ $addFields: { connectionId: "$_id" } },
{ $project: { uid: true, lastModified: true, connectionId: true } },
{
$unionWith: {
pipeline: [{ $documents: [{ uid }] }],
},
},
*/
{
$lookup: {
/* query users to get the friend data */
from: "users",
localField: "data.uid", //just uid if we remove the workaround above
foreignField: "uid",
as: "result",
let: {
lastModified: "$data.lastModified", //just $lastModified if we remove the workaround above
connectionId: "$data.connectionId", //just $connectionId if we remove the workaround above
},
pipeline: [
{
$project: {
_id: false,
uid: true,
connectionId: true,
name: true,
discordId: true,
discordAvatar: true,
startedTests: true,
completedTests: true,
timeTyping: true,
xp: true,
"streak.length": true,
"streak.maxLength": true,
personalBests: true,
"inventory.badges": true,
"premium.expirationTimestamp": true,
banned: 1,
lbOptOut: 1,
},
},
{
$addFields: {
lastModified: "$$lastModified",
connectionId: "$$connectionId",
top15: {
$reduce: {
//find highest wpm from time 15 PBs
input: "$personalBests.time.15",
initialValue: {},
in: {
$cond: [
{ $gte: ["$$this.wpm", "$$value.wpm"] },
"$$this",
"$$value",
],
},
},
},
top60: {
$reduce: {
//find highest wpm from time 60 PBs
input: "$personalBests.time.60",
initialValue: {},
in: {
$cond: [
{ $gte: ["$$this.wpm", "$$value.wpm"] },
"$$this",
"$$value",
],
},
},
},
badgeId: {
$ifNull: [
{
$first: {
$map: {
input: {
$filter: {
input: "$inventory.badges",
as: "badge",
cond: { $eq: ["$$badge.selected", true] },
},
},
as: "selectedBadge",
in: "$$selectedBadge.id",
},
},
},
"$$REMOVE",
],
},
isPremium: {
$cond: {
if: {
$or: [
{ $eq: ["$premium.expirationTimestamp", -1] },
{
$gt: [
"$premium.expirationTimestamp",
{ $toLong: "$$NOW" },
],
},
],
},
// oxlint-disable-next-line no-thenable
then: true,
else: "$$REMOVE",
},
},
},
},
{
$addFields: {
//remove nulls
top15: { $ifNull: ["$top15", "$$REMOVE"] },
top60: { $ifNull: ["$top60", "$$REMOVE"] },
badgeId: { $ifNull: ["$badgeId", "$$REMOVE"] },
lastModified: "$lastModified",
},
},
{
$project: {
personalBests: false,
inventory: false,
premium: false,
},
},
],
},
},
{
$replaceRoot: {
newRoot: {
$cond: [
{ $gt: [{ $size: "$result" }, 0] },
{ $first: "$result" },
{}, // empty document fallback, this can happen if the user is not present
],
},
},
},
])
.toArray()) as DBFriend[];
]
);
}