From b074e51c2e6936dbe16b1ea04571f65ff5d2ae1b Mon Sep 17 00:00:00 2001 From: Spencer Spenst Date: Wed, 27 Sep 2023 19:53:39 -0700 Subject: [PATCH 01/13] wip --- pages/api/collection-by-id/[id].ts | 57 ++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index 4c4ec8a5d..6ff03081c 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -1,11 +1,14 @@ +import cleanUser from '@root/lib/cleanUser'; +import { LEVEL_DEFAULT_PROJECTION } from '@root/models/schemas/levelSchema'; +import { PipelineStage, Types } from 'mongoose'; import type { NextApiRequest, NextApiResponse } from 'next'; import apiWrapper, { ValidObjectId } from '../../../helpers/apiWrapper'; -import { enrichLevels } from '../../../helpers/enrich'; +import { getEnrichLevelsPipelineSteps } from '../../../helpers/enrich'; import dbConnect from '../../../lib/dbConnect'; import { getUserFromToken } from '../../../lib/withAuth'; import Collection from '../../../models/db/collection'; import User from '../../../models/db/user'; -import { CollectionModel } from '../../../models/mongoose'; +import { CollectionModel, LevelModel } from '../../../models/mongoose'; export default apiWrapper({ GET: { @@ -30,22 +33,48 @@ export default apiWrapper({ }); export async function getCollectionById(id: string, reqUser: User | null) { - const collection = await CollectionModel.findById(id) - .populate({ - path: 'levels', - match: { isDraft: false }, - populate: { path: 'userId', model: 'User', select: 'name' }, - }) - .populate('userId', 'name'); + const collectionAgg = await CollectionModel.aggregate(([ + { + $match: { + _id: new Types.ObjectId(id), + }, + }, + { + $lookup: { + from: LevelModel.collection.name, + localField: 'levels', + foreignField: '_id', + as: 'levels', + pipeline: [ + { + $match: { + isDraft: false, + isDeleted: { + $ne: true + } + }, + }, + { + $project: { + ...LEVEL_DEFAULT_PROJECTION, + }, + }, + ...getEnrichLevelsPipelineSteps(reqUser, '_id', ''), + ], + }, + }, - if (!collection) { + ] as PipelineStage[])); + + if (!collectionAgg || collectionAgg.length === 0) { return null; } - const enrichedCollectionLevels = await enrichLevels(collection.levels, reqUser); - const newCollection = JSON.parse(JSON.stringify(collection)); + (collectionAgg[0] as Collection).levels?.map(level => { + cleanUser(level.userId); - newCollection.levels = enrichedCollectionLevels; + return level; + }); - return newCollection; + return collectionAgg[0]; } From 4b9ec1801e5aa691b3b5fd28354cbd3739e031ad Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Thu, 28 Sep 2023 09:23:24 -0400 Subject: [PATCH 02/13] fix issue with collection ordering --- pages/api/collection-by-id/[id].ts | 37 ++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index 6ff03081c..f08be842a 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -39,11 +39,27 @@ export async function getCollectionById(id: string, reqUser: User | null) { _id: new Types.ObjectId(id), }, }, + + { + $addFields: { + 'levelsWithSort': { + $map: { + input: '$levels', + as: 'item', + in: { + _id: '$$item', // making levels an array of objects with _id + } + } + } + } + }, { $lookup: { from: LevelModel.collection.name, - localField: 'levels', + localField: 'levelsWithSort._id', foreignField: '_id', + let: { 'orderedIds': '$levelsWithSort._id' }, + as: 'levels', pipeline: [ { @@ -54,16 +70,33 @@ export async function getCollectionById(id: string, reqUser: User | null) { } }, }, + { + $addFields: { + sort: { + $indexOfArray: [ '$$orderedIds', '$_id' ] + } + } + }, + { + $sort: { + sort: 1 + } + }, { $project: { ...LEVEL_DEFAULT_PROJECTION, }, }, + ...getEnrichLevelsPipelineSteps(reqUser, '_id', ''), ], }, }, - + { + $set: { + levelsWithSort: 0 + } + } ] as PipelineStage[])); if (!collectionAgg || collectionAgg.length === 0) { From ca940685942f8353b8403fdfc98461b942fd8abe Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Thu, 28 Sep 2023 09:27:03 -0400 Subject: [PATCH 03/13] fix test --- pages/api/collection-by-id/[id].ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index f08be842a..6c39adb49 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -1,5 +1,6 @@ import cleanUser from '@root/lib/cleanUser'; import { LEVEL_DEFAULT_PROJECTION } from '@root/models/schemas/levelSchema'; +import { USER_DEFAULT_PROJECTION } from '@root/models/schemas/userSchema'; import { PipelineStage, Types } from 'mongoose'; import type { NextApiRequest, NextApiResponse } from 'next'; import apiWrapper, { ValidObjectId } from '../../../helpers/apiWrapper'; @@ -8,7 +9,7 @@ import dbConnect from '../../../lib/dbConnect'; import { getUserFromToken } from '../../../lib/withAuth'; import Collection from '../../../models/db/collection'; import User from '../../../models/db/user'; -import { CollectionModel, LevelModel } from '../../../models/mongoose'; +import { CollectionModel, LevelModel, UserModel } from '../../../models/mongoose'; export default apiWrapper({ GET: { @@ -89,6 +90,24 @@ export async function getCollectionById(id: string, reqUser: User | null) { }, ...getEnrichLevelsPipelineSteps(reqUser, '_id', ''), + { + // populate user + $lookup: { + from: UserModel.collection.name, + localField: 'userId', + foreignField: '_id', + as: 'userId', + pipeline: [ + { $project: USER_DEFAULT_PROJECTION }, + ] + }, + }, + { + $unwind: { + path: '$userId', + preserveNullAndEmptyArrays: true, + }, + }, ], }, }, From ffd911a99ab3134d9605995053ad9a2e29428f1a Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Thu, 28 Sep 2023 22:38:00 -0400 Subject: [PATCH 04/13] improve getCollection across website --- pages/api/collection-by-id/[id].ts | 36 +++++-- pages/api/collection/[id].ts | 119 +-------------------- pages/collection/[username]/[slugName].tsx | 9 +- pages/edit/collection/[id].tsx | 4 +- 4 files changed, 37 insertions(+), 131 deletions(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index 6c39adb49..f5fc56f02 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -22,7 +22,7 @@ export default apiWrapper({ await dbConnect(); const token = req.cookies?.token; const reqUser = token ? await getUserFromToken(token, req) : null; - const collection = await getCollectionById(id as string, reqUser); + const collection = await getCollection2( { $match: { _id: new Types.ObjectId(id as string) } }, reqUser); if (!collection) { return res.status(404).json({ @@ -33,14 +33,31 @@ export default apiWrapper({ return res.status(200).json(collection); }); -export async function getCollectionById(id: string, reqUser: User | null) { +export async function getCollection2(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true) { const collectionAgg = await CollectionModel.aggregate(([ { - $match: { - _id: new Types.ObjectId(id), + + ...matchQuery, + + }, + { + // populate user for collection + $lookup: { + from: UserModel.collection.name, + localField: 'userId', + foreignField: '_id', + as: 'userId', + pipeline: [ + { $project: USER_DEFAULT_PROJECTION }, + ] + }, + }, + { + $unwind: { + path: '$userId', + preserveNullAndEmptyArrays: true, }, }, - { $addFields: { 'levelsWithSort': { @@ -65,7 +82,7 @@ export async function getCollectionById(id: string, reqUser: User | null) { pipeline: [ { $match: { - isDraft: false, + isDraft: noDraftLevels ? false : undefined, isDeleted: { $ne: true } @@ -112,10 +129,9 @@ export async function getCollectionById(id: string, reqUser: User | null) { }, }, { - $set: { - levelsWithSort: 0 - } - } + $unset: 'levelsWithSort' + }, + ] as PipelineStage[])); if (!collectionAgg || collectionAgg.length === 0) { diff --git a/pages/api/collection/[id].ts b/pages/api/collection/[id].ts index 826aabb70..2cf31869a 100644 --- a/pages/api/collection/[id].ts +++ b/pages/api/collection/[id].ts @@ -1,17 +1,12 @@ import { logger } from '@root/helpers/logger'; -import mongoose, { PipelineStage, Types } from 'mongoose'; +import mongoose, { Types } from 'mongoose'; import type { NextApiResponse } from 'next'; import { ValidObjectId, ValidObjectIdArray, ValidType } from '../../../helpers/apiWrapper'; -import { enrichLevels } from '../../../helpers/enrich'; import { generateCollectionSlug } from '../../../helpers/generateSlug'; -import dbConnect from '../../../lib/dbConnect'; import withAuth, { NextApiRequestWithAuth } from '../../../lib/withAuth'; import Collection from '../../../models/db/collection'; -import { EnrichedLevel } from '../../../models/db/level'; -import User from '../../../models/db/user'; -import { CollectionModel, LevelModel, UserModel } from '../../../models/mongoose'; -import { LEVEL_DEFAULT_PROJECTION } from '../../../models/schemas/levelSchema'; -import { USER_DEFAULT_PROJECTION } from '../../../models/schemas/userSchema'; +import { CollectionModel } from '../../../models/mongoose'; +import { getCollection2 } from '../collection-by-id/[id]'; type UpdateLevelParams = { authorNote?: string, @@ -20,112 +15,6 @@ type UpdateLevelParams = { slug?: string, } -export async function getCollection(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true) { - await dbConnect(); - - const levelsPipeline = []; - - if (noDraftLevels) { - levelsPipeline.push({ - $match: { - isDraft: false, - } - }); - } - - levelsPipeline.push( - { - $project: { - ...LEVEL_DEFAULT_PROJECTION - } - }, - { - $lookup: { - from: UserModel.collection.name, - localField: 'userId', - foreignField: '_id', - as: 'userId', - pipeline: [ - { - $project: { - ...USER_DEFAULT_PROJECTION - } - } - ] - } - }, - { - $unwind: { - path: '$userId', - preserveNullAndEmptyArrays: true, - } - }, - ); - - const collectionAgg = await CollectionModel.aggregate([ - { - ...matchQuery, - }, - { - $lookup: { - as: 'levelsPopulated', - foreignField: '_id', - from: LevelModel.collection.name, - localField: 'levels', - pipeline: levelsPipeline, - }, - }, - { - $lookup: { - from: UserModel.collection.name, - localField: 'userId', - foreignField: '_id', - as: 'userId', - pipeline: [ - { - $project: { - ...USER_DEFAULT_PROJECTION - } - } - ] - } - }, - { - $unwind: { - path: '$userId', - preserveNullAndEmptyArrays: true, - } - }, - ]); - - if (collectionAgg.length !== 1) { - return null; - } - - const collection = collectionAgg[0]; - const levelMap = new Map(); - - if (collection.levelsPopulated) { - for (const level of collection.levelsPopulated) { - levelMap.set(level._id.toString(), level); - } - - collection.levels = collection.levels.map((level: EnrichedLevel) => { - return levelMap.get(level._id.toString()) as EnrichedLevel; - }); - // remove undefined levels - collection.levels = collection.levels.filter((level: EnrichedLevel) => level !== undefined); - collection.levelsPopulated = undefined; - } - - const enrichedCollectionLevels = await enrichLevels(collection.levels, reqUser); - const newCollection = JSON.parse(JSON.stringify(collection)) as Collection; - - newCollection.levels = enrichedCollectionLevels; - - return newCollection; -} - export default withAuth({ GET: { query: { @@ -151,7 +40,7 @@ export default withAuth({ if (req.method === 'GET') { const { id } = req.query; - const collection = await getCollection({ $match: { + const collection = await getCollection2({ $match: { _id: new Types.ObjectId(id as string), userId: req.user._id, } }, req.user, false); diff --git a/pages/collection/[username]/[slugName].tsx b/pages/collection/[username]/[slugName].tsx index 90bf8eab3..4440ab432 100644 --- a/pages/collection/[username]/[slugName].tsx +++ b/pages/collection/[username]/[slugName].tsx @@ -1,5 +1,6 @@ import FormattedUser from '@root/components/formatted/formattedUser'; import StatFilter from '@root/constants/statFilter'; +import { getCollection2 } from '@root/pages/api/collection-by-id/[id]'; import { GetServerSidePropsContext, NextApiRequest } from 'next'; import Link from 'next/link'; import { NextSeo } from 'next-seo'; @@ -22,7 +23,6 @@ import { EnrichedCollection } from '../../../models/db/collection'; import { EnrichedLevel } from '../../../models/db/level'; import SelectOption from '../../../models/selectOption'; import SelectOptionStats from '../../../models/selectOptionStats'; -import { getCollection } from '../../api/collection/[id]'; interface CollectionUrlQueryParams extends ParsedUrlQuery { slugName: string; @@ -54,9 +54,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const token = context.req?.cookies?.token; const reqUser = token ? await getUserFromToken(token, context.req as NextApiRequest) : null; - const collection = await getCollection({ $match: { slug: username + '/' + slugName } }, reqUser); - if (!collection) { + const collectionAgg = await getCollection2({ $match: { slug: username + '/' + slugName } }, reqUser); + + if (!collectionAgg) { logger.error('CollectionModel.find returned null in pages/collection'); return { @@ -66,7 +67,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { return { props: { - collection: JSON.parse(JSON.stringify(collection)), + collection: JSON.parse(JSON.stringify(collectionAgg)), } as CollectionProps }; } diff --git a/pages/edit/collection/[id].tsx b/pages/edit/collection/[id].tsx index 8fba1879b..d291a5cf1 100644 --- a/pages/edit/collection/[id].tsx +++ b/pages/edit/collection/[id].tsx @@ -1,3 +1,4 @@ +import { getCollection2 } from '@root/pages/api/collection-by-id/[id]'; import { Types } from 'mongoose'; import { GetServerSidePropsContext, NextApiRequest } from 'next'; import nProgress from 'nprogress'; @@ -13,7 +14,6 @@ import { EnrichedLevel } from '../../../models/db/level'; import User from '../../../models/db/user'; import SelectOption from '../../../models/selectOption'; import SelectOptionStats from '../../../models/selectOptionStats'; -import { getCollection } from '../../api/collection/[id]'; export async function getServerSideProps(context: GetServerSidePropsContext) { const token = context.req?.cookies?.token; @@ -29,7 +29,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const collection = await getCollection({ $match: { + const collection = await getCollection2({ $match: { _id: new Types.ObjectId(id as string), userId: reqUser._id, } }, reqUser, false); From bffe35474f9732f340717a61c0e8d986d8f17fc3 Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Thu, 28 Sep 2023 22:56:00 -0400 Subject: [PATCH 05/13] improving agg for edit levels --- components/cards/draggableSelectCard.tsx | 2 ++ pages/api/collection-by-id/[id].ts | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/components/cards/draggableSelectCard.tsx b/components/cards/draggableSelectCard.tsx index 8a966cffa..1498dfebf 100644 --- a/components/cards/draggableSelectCard.tsx +++ b/components/cards/draggableSelectCard.tsx @@ -97,6 +97,8 @@ export default function DraggableSelectCard({ height: option.height ?? Dimensions.OptionHeight, textShadow: '1px 1px black', width: Dimensions.OptionWidth, + // dotted border if draft + borderStyle: option.level?.isDraft ? 'dotted' : undefined, }} > diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index f5fc56f02..030d084a9 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -36,9 +36,7 @@ export default apiWrapper({ export async function getCollection2(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true) { const collectionAgg = await CollectionModel.aggregate(([ { - ...matchQuery, - }, { // populate user for collection @@ -82,7 +80,11 @@ export async function getCollection2(matchQuery: PipelineStage, reqUser: User | pipeline: [ { $match: { - isDraft: noDraftLevels ? false : undefined, + ...(noDraftLevels ? { + isDraft: { + $ne: true + } + } : undefined), isDeleted: { $ne: true } @@ -103,6 +105,11 @@ export async function getCollection2(matchQuery: PipelineStage, reqUser: User | { $project: { ...LEVEL_DEFAULT_PROJECTION, + ...(noDraftLevels ? { + + } : { + isDraft: 1, + }), }, }, From 487da08fa2f5cf5665b59805db107ac0630f7530 Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Fri, 29 Sep 2023 08:36:39 -0400 Subject: [PATCH 06/13] rename --- pages/api/collection-by-id/[id].ts | 4 ++-- pages/api/collection/[id].ts | 4 ++-- pages/collection/[username]/[slugName].tsx | 4 ++-- pages/edit/collection/[id].tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index 030d084a9..0e7140568 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -22,7 +22,7 @@ export default apiWrapper({ await dbConnect(); const token = req.cookies?.token; const reqUser = token ? await getUserFromToken(token, req) : null; - const collection = await getCollection2( { $match: { _id: new Types.ObjectId(id as string) } }, reqUser); + const collection = await getCollection( { $match: { _id: new Types.ObjectId(id as string) } }, reqUser); if (!collection) { return res.status(404).json({ @@ -33,7 +33,7 @@ export default apiWrapper({ return res.status(200).json(collection); }); -export async function getCollection2(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true) { +export async function getCollection(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true) { const collectionAgg = await CollectionModel.aggregate(([ { ...matchQuery, diff --git a/pages/api/collection/[id].ts b/pages/api/collection/[id].ts index 2cf31869a..5b384835f 100644 --- a/pages/api/collection/[id].ts +++ b/pages/api/collection/[id].ts @@ -6,7 +6,7 @@ import { generateCollectionSlug } from '../../../helpers/generateSlug'; import withAuth, { NextApiRequestWithAuth } from '../../../lib/withAuth'; import Collection from '../../../models/db/collection'; import { CollectionModel } from '../../../models/mongoose'; -import { getCollection2 } from '../collection-by-id/[id]'; +import { getCollection } from '../collection-by-id/[id]'; type UpdateLevelParams = { authorNote?: string, @@ -40,7 +40,7 @@ export default withAuth({ if (req.method === 'GET') { const { id } = req.query; - const collection = await getCollection2({ $match: { + const collection = await getCollection({ $match: { _id: new Types.ObjectId(id as string), userId: req.user._id, } }, req.user, false); diff --git a/pages/collection/[username]/[slugName].tsx b/pages/collection/[username]/[slugName].tsx index 4440ab432..b679382d9 100644 --- a/pages/collection/[username]/[slugName].tsx +++ b/pages/collection/[username]/[slugName].tsx @@ -1,6 +1,6 @@ import FormattedUser from '@root/components/formatted/formattedUser'; import StatFilter from '@root/constants/statFilter'; -import { getCollection2 } from '@root/pages/api/collection-by-id/[id]'; +import { getCollection } from '@root/pages/api/collection-by-id/[id]'; import { GetServerSidePropsContext, NextApiRequest } from 'next'; import Link from 'next/link'; import { NextSeo } from 'next-seo'; @@ -55,7 +55,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const token = context.req?.cookies?.token; const reqUser = token ? await getUserFromToken(token, context.req as NextApiRequest) : null; - const collectionAgg = await getCollection2({ $match: { slug: username + '/' + slugName } }, reqUser); + const collectionAgg = await getCollection({ $match: { slug: username + '/' + slugName } }, reqUser); if (!collectionAgg) { logger.error('CollectionModel.find returned null in pages/collection'); diff --git a/pages/edit/collection/[id].tsx b/pages/edit/collection/[id].tsx index d291a5cf1..dde6d1346 100644 --- a/pages/edit/collection/[id].tsx +++ b/pages/edit/collection/[id].tsx @@ -1,4 +1,4 @@ -import { getCollection2 } from '@root/pages/api/collection-by-id/[id]'; +import { getCollection } from '@root/pages/api/collection-by-id/[id]'; import { Types } from 'mongoose'; import { GetServerSidePropsContext, NextApiRequest } from 'next'; import nProgress from 'nprogress'; @@ -29,7 +29,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const collection = await getCollection2({ $match: { + const collection = await getCollection({ $match: { _id: new Types.ObjectId(id as string), userId: reqUser._id, } }, reqUser, false); From 01aa29826c794d15208e010e4f1c01e577091820 Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Fri, 29 Sep 2023 10:23:41 -0400 Subject: [PATCH 07/13] improve profile collections loading by ~25% --- pages/api/collection-by-id/[id].ts | 66 ++++++++++++++++++++--- pages/profile/[name]/[[...tab]]/index.tsx | 14 ++--- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index 0e7140568..9f90a137d 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -9,7 +9,7 @@ import dbConnect from '../../../lib/dbConnect'; import { getUserFromToken } from '../../../lib/withAuth'; import Collection from '../../../models/db/collection'; import User from '../../../models/db/user'; -import { CollectionModel, LevelModel, UserModel } from '../../../models/mongoose'; +import { CollectionModel, LevelModel, StatModel, UserModel } from '../../../models/mongoose'; export default apiWrapper({ GET: { @@ -34,6 +34,16 @@ export default apiWrapper({ }); export async function getCollection(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true) { + const collections = await getCollections(matchQuery, reqUser, noDraftLevels); + + if (collections.length === 0) { + return null; + } + + return collections[0] as Collection; +} + +export async function getCollections(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true): Promise { const collectionAgg = await CollectionModel.aggregate(([ { ...matchQuery, @@ -104,7 +114,7 @@ export async function getCollection(matchQuery: PipelineStage, reqUser: User | n }, { $project: { - ...LEVEL_DEFAULT_PROJECTION, + leastMoves: 1, ...(noDraftLevels ? { } : { @@ -113,7 +123,41 @@ export async function getCollection(matchQuery: PipelineStage, reqUser: User | n }, }, - ...getEnrichLevelsPipelineSteps(reqUser, '_id', ''), + { + $lookup: { + from: StatModel.collection.name, + localField: '_id', + foreignField: 'levelId', + as: 'stats', + pipeline: [ + { + $match: { + userId: reqUser?._id, + complete: true + }, + }, + { + $project: { + _id: 0, + // project complete to 1 if it exists, otherwise 0 + complete: { + $cond: { + if: { $eq: ['$complete', true] }, + then: 1, + else: 0 + } + } + } + } + ] + }, + }, + { + $unwind: { + path: '$stats', + preserveNullAndEmptyArrays: true + } + }, { // populate user $lookup: { @@ -138,18 +182,24 @@ export async function getCollection(matchQuery: PipelineStage, reqUser: User | n { $unset: 'levelsWithSort' }, + { + $addFields: { + levelCount: { + $size: '$levels' + }, + userSolvedCount: { + $sum: '$levels.stats.complete' + } + } + } ] as PipelineStage[])); - if (!collectionAgg || collectionAgg.length === 0) { - return null; - } - (collectionAgg[0] as Collection).levels?.map(level => { cleanUser(level.userId); return level; }); - return collectionAgg[0]; + return collectionAgg; } diff --git a/pages/profile/[name]/[[...tab]]/index.tsx b/pages/profile/[name]/[[...tab]]/index.tsx index f81d7af41..ff9efc300 100644 --- a/pages/profile/[name]/[[...tab]]/index.tsx +++ b/pages/profile/[name]/[[...tab]]/index.tsx @@ -11,6 +11,7 @@ import { getUsersWithMultiplayerProfile } from '@root/helpers/getUsersWithMultip import useSWRHelper from '@root/hooks/useSWRHelper'; import Graph from '@root/models/db/graph'; import { MultiplayerMatchState } from '@root/models/MultiplayerEnums'; +import { getCollections } from '@root/pages/api/collection-by-id/[id]'; import classNames from 'classnames'; import { debounce } from 'debounce'; import { GetServerSidePropsContext, NextApiRequest } from 'next'; @@ -163,16 +164,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { } if (profileTab === ProfileTab.Collections) { - const collections = await CollectionModel.find({ userId: user._id }, 'levels name slug') - .populate({ - path: 'levels', - select: '_id leastMoves', - match: { isDraft: false }, - }) - .sort({ name: 1 }); - const enrichedCollections = await Promise.all(collections.map(collection => enrichCollection(collection, reqUser))); - - profilePageProps.enrichedCollections = JSON.parse(JSON.stringify(enrichedCollections)); + const collectionsAgg = await getCollections({ $match: { userId: user._id } }, reqUser); + + profilePageProps.enrichedCollections = JSON.parse(JSON.stringify(collectionsAgg)); } if (profileTab === ProfileTab.Levels) { From e0a4782d041e0a0f2466294efdfe5f79901e1d9f Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Fri, 29 Sep 2023 11:15:39 -0400 Subject: [PATCH 08/13] fix some issues with getCollections --- pages/api/collection-by-id/[id].ts | 41 ++++++++++++++-------- pages/api/collection/[id].ts | 13 ++++--- pages/campaign/[slug].tsx | 22 ++++++------ pages/collection/[username]/[slugName].tsx | 6 +++- pages/edit/collection/[id].tsx | 16 ++++++--- pages/profile/[name]/[[...tab]]/index.tsx | 5 ++- 6 files changed, 68 insertions(+), 35 deletions(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index 9f90a137d..a97f0acd9 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -22,7 +22,10 @@ export default apiWrapper({ await dbConnect(); const token = req.cookies?.token; const reqUser = token ? await getUserFromToken(token, req) : null; - const collection = await getCollection( { $match: { _id: new Types.ObjectId(id as string) } }, reqUser); + const collection = await getCollection( { + matchQuery: { $match: { _id: new Types.ObjectId(id as string) } }, + reqUser + }); if (!collection) { return res.status(404).json({ @@ -33,8 +36,15 @@ export default apiWrapper({ return res.status(200).json(collection); }); -export async function getCollection(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true) { - const collections = await getCollections(matchQuery, reqUser, noDraftLevels); +interface GetCollectionProps { + matchQuery: PipelineStage, + reqUser: User | null, + includeDraft?: boolean, + populateLevels?: boolean, +} + +export async function getCollection(props: GetCollectionProps): Promise { + const collections = await getCollections(props); if (collections.length === 0) { return null; @@ -43,7 +53,7 @@ export async function getCollection(matchQuery: PipelineStage, reqUser: User | n return collections[0] as Collection; } -export async function getCollections(matchQuery: PipelineStage, reqUser: User | null, noDraftLevels = true): Promise { +export async function getCollections({ matchQuery, reqUser, includeDraft, populateLevels }: GetCollectionProps): Promise { const collectionAgg = await CollectionModel.aggregate(([ { ...matchQuery, @@ -90,7 +100,7 @@ export async function getCollections(matchQuery: PipelineStage, reqUser: User | pipeline: [ { $match: { - ...(noDraftLevels ? { + ...(!includeDraft ? { isDraft: { $ne: true } @@ -115,15 +125,15 @@ export async function getCollections(matchQuery: PipelineStage, reqUser: User | { $project: { leastMoves: 1, - ...(noDraftLevels ? { - - } : { - isDraft: 1, - }), + ...(includeDraft ? { + isDraft: 1 + } : {}), + ...(populateLevels ? { + ...LEVEL_DEFAULT_PROJECTION + } : {}) }, }, - - { + ...(populateLevels ? getEnrichLevelsPipelineSteps(reqUser, '_id', '') : [{ $lookup: { from: StatModel.collection.name, localField: '_id', @@ -151,7 +161,7 @@ export async function getCollections(matchQuery: PipelineStage, reqUser: User | } ] }, - }, + }]), { $unwind: { path: '$stats', @@ -182,6 +192,7 @@ export async function getCollections(matchQuery: PipelineStage, reqUser: User | { $unset: 'levelsWithSort' }, + { $addFields: { levelCount: { @@ -191,10 +202,12 @@ export async function getCollections(matchQuery: PipelineStage, reqUser: User | $sum: '$levels.stats.complete' } } - } + }, + ...(!populateLevels ? [{ $unset: 'levels' }] : []), ] as PipelineStage[])); + cleanUser(collectionAgg[0]?.userId); (collectionAgg[0] as Collection).levels?.map(level => { cleanUser(level.userId); diff --git a/pages/api/collection/[id].ts b/pages/api/collection/[id].ts index 5b384835f..92929b885 100644 --- a/pages/api/collection/[id].ts +++ b/pages/api/collection/[id].ts @@ -40,10 +40,15 @@ export default withAuth({ if (req.method === 'GET') { const { id } = req.query; - const collection = await getCollection({ $match: { - _id: new Types.ObjectId(id as string), - userId: req.user._id, - } }, req.user, false); + const collection = await getCollection({ + matchQuery: { + $match: { + _id: new Types.ObjectId(id as string), + userId: req.user._id, + } + }, reqUser: req.user, + includeDraft: true + }); if (!collection) { return res.status(404).json({ diff --git a/pages/campaign/[slug].tsx b/pages/campaign/[slug].tsx index ff4f26faa..d13eca6f3 100644 --- a/pages/campaign/[slug].tsx +++ b/pages/campaign/[slug].tsx @@ -1,3 +1,4 @@ +import { Types } from 'mongoose'; import { GetServerSidePropsContext, NextApiRequest } from 'next'; import { ParsedUrlQuery } from 'querystring'; import React, { useCallback } from 'react'; @@ -14,6 +15,7 @@ import { EnrichedCollection } from '../../models/db/collection'; import { CampaignModel } from '../../models/mongoose'; import SelectOption from '../../models/selectOption'; import SelectOptionStats from '../../models/selectOptionStats'; +import { getCollections } from '../api/collection-by-id/[id]'; interface CampaignUrlQueryParams extends ParsedUrlQuery { slug: string; @@ -51,16 +53,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const token = context.req?.cookies?.token; const reqUser = token ? await getUserFromToken(token, context.req as NextApiRequest) : null; - const campaign = await CampaignModel.findOne({ slug: slug }) - .populate({ - path: 'collections', - populate: { - match: { isDraft: false }, - path: 'levels', - select: '_id leastMoves', - }, - select: '_id levels name slug', - }).sort({ name: 1 }); + const campaign = await CampaignModel.findOne({ slug: slug }); if (!campaign) { logger.error('CampaignModel.find returned null in pages/campaign'); @@ -70,7 +63,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const enrichedCollections = await Promise.all(campaign.collections.map(collection => enrichCollection(collection, reqUser))); + const enrichedCollections = await getCollections( + { + matchQuery: { $match: { _id: { $in: campaign.collections.map(collection => new Types.ObjectId(collection._id)) } } }, + reqUser: reqUser, + + }, + + ); return { props: { diff --git a/pages/collection/[username]/[slugName].tsx b/pages/collection/[username]/[slugName].tsx index b679382d9..765ca2d4f 100644 --- a/pages/collection/[username]/[slugName].tsx +++ b/pages/collection/[username]/[slugName].tsx @@ -55,7 +55,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const token = context.req?.cookies?.token; const reqUser = token ? await getUserFromToken(token, context.req as NextApiRequest) : null; - const collectionAgg = await getCollection({ $match: { slug: username + '/' + slugName } }, reqUser); + const collectionAgg = await getCollection({ + matchQuery: { $match: { slug: username + '/' + slugName } }, + reqUser, + populateLevels: true, + }); if (!collectionAgg) { logger.error('CollectionModel.find returned null in pages/collection'); diff --git a/pages/edit/collection/[id].tsx b/pages/edit/collection/[id].tsx index dde6d1346..3cface1cf 100644 --- a/pages/edit/collection/[id].tsx +++ b/pages/edit/collection/[id].tsx @@ -29,10 +29,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const collection = await getCollection({ $match: { - _id: new Types.ObjectId(id as string), - userId: reqUser._id, - } }, reqUser, false); + const collection = await getCollection( + { + matchQuery: { + $match: { + _id: new Types.ObjectId(id as string), + userId: reqUser._id, + } + }, + reqUser, + populateLevels: true, + includeDraft: true + }); if (!collection) { return { diff --git a/pages/profile/[name]/[[...tab]]/index.tsx b/pages/profile/[name]/[[...tab]]/index.tsx index ff9efc300..862887068 100644 --- a/pages/profile/[name]/[[...tab]]/index.tsx +++ b/pages/profile/[name]/[[...tab]]/index.tsx @@ -164,7 +164,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { } if (profileTab === ProfileTab.Collections) { - const collectionsAgg = await getCollections({ $match: { userId: user._id } }, reqUser); + const collectionsAgg = await getCollections({ + matchQuery: { $match: { userId: user._id } }, + reqUser, + }); profilePageProps.enrichedCollections = JSON.parse(JSON.stringify(collectionsAgg)); } From ac0f471722b537f173180710ccc0161131a1873f Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Fri, 29 Sep 2023 11:18:16 -0400 Subject: [PATCH 09/13] fix tests --- pages/api/collection-by-id/[id].ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index a97f0acd9..e448d0808 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -24,7 +24,8 @@ export default apiWrapper({ const reqUser = token ? await getUserFromToken(token, req) : null; const collection = await getCollection( { matchQuery: { $match: { _id: new Types.ObjectId(id as string) } }, - reqUser + reqUser, + populateLevels: true, }); if (!collection) { From 2483aa902059d5e0e206c2ab77be51256b0f493c Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Fri, 29 Sep 2023 11:20:46 -0400 Subject: [PATCH 10/13] fix bug found from broken test. --- pages/api/collection-by-id/[id].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index e448d0808..abcc38f93 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -209,7 +209,7 @@ export async function getCollections({ matchQuery, reqUser, includeDraft, popula ] as PipelineStage[])); cleanUser(collectionAgg[0]?.userId); - (collectionAgg[0] as Collection).levels?.map(level => { + (collectionAgg[0] as Collection)?.levels?.map(level => { cleanUser(level.userId); return level; From 5d391ed4f6fd4980f6e83e2934afb19b5cff47a9 Mon Sep 17 00:00:00 2001 From: Danny Miller Date: Fri, 29 Sep 2023 11:32:04 -0400 Subject: [PATCH 11/13] knock another 100ms off profile load --- pages/profile/[name]/[[...tab]]/index.tsx | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pages/profile/[name]/[[...tab]]/index.tsx b/pages/profile/[name]/[[...tab]]/index.tsx index 862887068..9d358794b 100644 --- a/pages/profile/[name]/[[...tab]]/index.tsx +++ b/pages/profile/[name]/[[...tab]]/index.tsx @@ -147,13 +147,24 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { if (profileTab === ProfileTab.Profile) { if (reqUser && reqUser._id.toString() === userId) { - const followingGraph = await GraphModel.find({ - source: reqUser._id, - type: GraphType.FOLLOW, - }, 'target targetModel createdAt').populate('target', 'name avatarUpdatedAt last_visited_at hideStatus').exec() as Graph[]; - - /* istanbul ignore next */ - const reqUserFollowing = followingGraph.map((f) => { + // make a aggregation version of the same query above + const followingAgg = await GraphModel.aggregate([ + { $match: { source: reqUser._id, type: GraphType.FOLLOW } }, + { + $lookup: { + from: 'users', + localField: 'target', + foreignField: '_id', + as: 'target', + pipeline: [ + { $project: { name: 1, avatarUpdatedAt: 1, last_visited_at: 1, hideStatus: 1 } }, + ], + }, + }, + { $unwind: '$target' }, + { $sort: { createdAt: -1 } }, + ]).exec() as Graph[]; + const reqUserFollowing = followingAgg.map((f) => { cleanUser(f.target as User); return f; From 01dc2eff833ec19a0d1d810a9679498a788a3446 Mon Sep 17 00:00:00 2001 From: Spencer Spenst Date: Fri, 29 Sep 2023 17:22:05 -0700 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=91=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/cards/draggableSelectCard.tsx | 4 ++-- pages/api/collection-by-id/[id].ts | 3 --- pages/campaign/[slug].tsx | 3 --- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/components/cards/draggableSelectCard.tsx b/components/cards/draggableSelectCard.tsx index 1498dfebf..ed902d25e 100644 --- a/components/cards/draggableSelectCard.tsx +++ b/components/cards/draggableSelectCard.tsx @@ -93,12 +93,12 @@ export default function DraggableSelectCard({ style={{ backgroundColor: spec.isOver ? 'var(--bg-color-4)' : undefined, borderColor: color, + // dotted border if draft + borderStyle: option.level?.isDraft ? 'dashed' : undefined, color: color, height: option.height ?? Dimensions.OptionHeight, textShadow: '1px 1px black', width: Dimensions.OptionWidth, - // dotted border if draft - borderStyle: option.level?.isDraft ? 'dotted' : undefined, }} > diff --git a/pages/api/collection-by-id/[id].ts b/pages/api/collection-by-id/[id].ts index abcc38f93..6b47c1f49 100644 --- a/pages/api/collection-by-id/[id].ts +++ b/pages/api/collection-by-id/[id].ts @@ -96,7 +96,6 @@ export async function getCollections({ matchQuery, reqUser, includeDraft, popula localField: 'levelsWithSort._id', foreignField: '_id', let: { 'orderedIds': '$levelsWithSort._id' }, - as: 'levels', pipeline: [ { @@ -193,7 +192,6 @@ export async function getCollections({ matchQuery, reqUser, includeDraft, popula { $unset: 'levelsWithSort' }, - { $addFields: { levelCount: { @@ -205,7 +203,6 @@ export async function getCollections({ matchQuery, reqUser, includeDraft, popula } }, ...(!populateLevels ? [{ $unset: 'levels' }] : []), - ] as PipelineStage[])); cleanUser(collectionAgg[0]?.userId); diff --git a/pages/campaign/[slug].tsx b/pages/campaign/[slug].tsx index d13eca6f3..1c2ecad36 100644 --- a/pages/campaign/[slug].tsx +++ b/pages/campaign/[slug].tsx @@ -6,7 +6,6 @@ import Select from '../../components/cards/select'; import formattedAuthorNote from '../../components/formatted/formattedAuthorNote'; import LinkInfo from '../../components/formatted/linkInfo'; import Page from '../../components/page/page'; -import { enrichCollection } from '../../helpers/enrich'; import { logger } from '../../helpers/logger'; import dbConnect from '../../lib/dbConnect'; import { getUserFromToken } from '../../lib/withAuth'; @@ -67,9 +66,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { { matchQuery: { $match: { _id: { $in: campaign.collections.map(collection => new Types.ObjectId(collection._id)) } } }, reqUser: reqUser, - }, - ); return { From 49f3e5deb10a370c0e33cfb1750dd2a1d1b2baeb Mon Sep 17 00:00:00 2001 From: Spencer Spenst Date: Fri, 29 Sep 2023 17:22:26 -0700 Subject: [PATCH 13/13] forgot 1 file --- pages/profile/[name]/[[...tab]]/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/profile/[name]/[[...tab]]/index.tsx b/pages/profile/[name]/[[...tab]]/index.tsx index 9d358794b..9ff7b9666 100644 --- a/pages/profile/[name]/[[...tab]]/index.tsx +++ b/pages/profile/[name]/[[...tab]]/index.tsx @@ -35,7 +35,6 @@ import ProfileInsights from '../../../../components/profile/profileInsights'; import Dimensions from '../../../../constants/dimensions'; import GraphType from '../../../../constants/graphType'; import TimeRange from '../../../../constants/timeRange'; -import { enrichCollection } from '../../../../helpers/enrich'; import statFilterOptions from '../../../../helpers/filterSelectOptions'; import getProfileSlug from '../../../../helpers/getProfileSlug'; import { getReviewsByUserId, getReviewsByUserIdCount } from '../../../../helpers/getReviewsByUserId'; @@ -44,7 +43,7 @@ import naturalSort from '../../../../helpers/naturalSort'; import cleanUser from '../../../../lib/cleanUser'; import { getUserFromToken } from '../../../../lib/withAuth'; import Achievement from '../../../../models/db/achievement'; -import Collection, { EnrichedCollection } from '../../../../models/db/collection'; +import { EnrichedCollection } from '../../../../models/db/collection'; import { EnrichedLevel } from '../../../../models/db/level'; import Review from '../../../../models/db/review'; import User from '../../../../models/db/user';