Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

collection-by-id aggregate #1003

Merged
merged 15 commits into from
Sep 30, 2023
2 changes: 2 additions & 0 deletions components/cards/draggableSelectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ 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',
Expand Down
197 changes: 181 additions & 16 deletions pages/api/collection-by-id/[id].ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
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';
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, StatModel, UserModel } from '../../../models/mongoose';

export default apiWrapper({
GET: {
Expand All @@ -18,7 +22,11 @@ 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 getCollection( {
matchQuery: { $match: { _id: new Types.ObjectId(id as string) } },
reqUser,
populateLevels: true,
});

if (!collection) {
return res.status(404).json({
Expand All @@ -29,23 +37,180 @@ export default apiWrapper({
return res.status(200).json(collection);
});

export async function getCollectionById(id: string, reqUser: User | null) {
const collection = await CollectionModel.findById<Collection>(id)
.populate({
path: 'levels',
match: { isDraft: false },
populate: { path: 'userId', model: 'User', select: 'name' },
})
.populate('userId', 'name');
interface GetCollectionProps {
matchQuery: PipelineStage,
reqUser: User | null,
includeDraft?: boolean,
populateLevels?: boolean,
}

if (!collection) {
export async function getCollection(props: GetCollectionProps): Promise<Collection | null> {
const collections = await getCollections(props);

if (collections.length === 0) {
return null;
}

const enrichedCollectionLevels = await enrichLevels(collection.levels, reqUser);
const newCollection = JSON.parse(JSON.stringify(collection));
return collections[0] as Collection;
}

export async function getCollections({ matchQuery, reqUser, includeDraft, populateLevels }: GetCollectionProps): Promise<Collection[]> {
const collectionAgg = await CollectionModel.aggregate(([
{
...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': {
$map: {
input: '$levels',
as: 'item',
in: {
_id: '$$item', // making levels an array of objects with _id
}
}
}
}
},
{
$lookup: {
from: LevelModel.collection.name,
localField: 'levelsWithSort._id',
foreignField: '_id',
let: { 'orderedIds': '$levelsWithSort._id' },
as: 'levels',
pipeline: [
{
$match: {
...(!includeDraft ? {
isDraft: {
$ne: true
}
} : undefined),
isDeleted: {
$ne: true
}
},
},
{
$addFields: {
sort: {
$indexOfArray: [ '$$orderedIds', '$_id' ]
}
}
},
{
$sort: {
sort: 1
}
},
{
$project: {
leastMoves: 1,
...(includeDraft ? {
isDraft: 1
} : {}),
...(populateLevels ? {
...LEVEL_DEFAULT_PROJECTION
} : {})
},
},
...(populateLevels ? 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: {
from: UserModel.collection.name,
localField: 'userId',
foreignField: '_id',
as: 'userId',
pipeline: [
{ $project: USER_DEFAULT_PROJECTION },
]
},
},
{
$unwind: {
path: '$userId',
preserveNullAndEmptyArrays: true,
},
},
],
},
},
{
$unset: 'levelsWithSort'
},
{
$addFields: {
levelCount: {
$size: '$levels'
},
userSolvedCount: {
$sum: '$levels.stats.complete'
}
}
},
...(!populateLevels ? [{ $unset: 'levels' }] : []),
] as PipelineStage[]));

cleanUser(collectionAgg[0]?.userId);
(collectionAgg[0] as Collection)?.levels?.map(level => {
cleanUser(level.userId);

newCollection.levels = enrichedCollectionLevels;
return level;
});

return newCollection;
return collectionAgg;
}
130 changes: 12 additions & 118 deletions pages/api/collection/[id].ts
Original file line number Diff line number Diff line change
@@ -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 { getCollection } from '../collection-by-id/[id]';

type UpdateLevelParams = {
authorNote?: string,
Expand All @@ -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<Collection>([
{
...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<string, EnrichedLevel>();

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: {
Expand All @@ -151,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({
Expand Down
Loading