Adds Collections model and Editor API to manage collections

- List any user's collections
- Create new collection
- Modify collection metadata
- Delete collection
- Add/remove any project to/from a collection
This commit is contained in:
Andrew Nicolaou 2019-07-08 12:06:13 +02:00
parent 0ae7a9eebb
commit 83978acc1d
10 changed files with 395 additions and 0 deletions

View file

@ -0,0 +1,74 @@
import Collection from '../../models/collection';
import Project from '../../models/project';
export default function addProjectToCollection(req, res) {
const owner = req.user._id;
const { id: collectionId, projectId } = req.params;
const collectionPromise = Collection.findById(collectionId).populate('items.project', '_id');
const projectPromise = Project.findById(projectId);
function sendFailure(code, message) {
res.status(code).json({ success: false, message });
}
function sendSuccess(collection) {
res.status(200).json(collection);
}
function updateCollection([collection, project]) {
if (collection == null) {
sendFailure(404, 'Collection not found');
return null;
}
if (project == null) {
sendFailure(404, 'Project not found');
return null;
}
if (!collection.owner.equals(owner)) {
sendFailure(403, 'User does not own this collection');
return null;
}
const projectInCollection = collection.items.find(p => p.project._id === project._id);
if (projectInCollection) {
sendFailure(404, 'Project already in collection');
return null;
}
try {
collection.items.push({ project });
return collection.save();
} catch (error) {
console.error(error);
sendFailure(500, error.message);
return null;
}
}
function populateReferences(collection) {
return Collection.populate(
collection,
[
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
populate: {
path: 'user', select: ['username']
}
}
]
);
}
return Promise.all([collectionPromise, projectPromise])
.then(updateCollection)
.then(populateReferences)
.then(sendSuccess)
.catch(sendFailure);
}

View file

@ -0,0 +1,47 @@
import Collection from '../../models/collection';
export default function createCollection(req, res) {
const owner = req.user._id;
const { name, description, slug } = req.body;
const values = {
owner,
name,
description,
slug
};
function sendFailure({ code = 500, message = 'Something went wrong' }) {
res.status(code).json({ success: false, message });
}
function sendSuccess(newCollection) {
res.json(newCollection);
}
function populateReferences(newCollection) {
return Collection.populate(
newCollection,
[
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
populate: {
path: 'user', select: ['username']
}
}
]
);
}
if (owner == null) {
sendFailure({ code: 404, message: 'No user specified' });
return null;
}
return Collection.create(values)
.then(populateReferences)
.then(sendSuccess)
.catch(sendFailure);
}

View file

@ -0,0 +1,6 @@
export { default as addProjectToCollection } from './addProjectToCollection';
export { default as createCollection } from './createCollection';
export { default as listCollections } from './listCollections';
export { default as removeCollection } from './removeCollection';
export { default as removeProjectFromCollection } from './removeProjectFromCollection';
export { default as updateCollection } from './updateCollection';

View file

@ -0,0 +1,48 @@
import Collection from '../../models/collection';
import User from '../../models/user';
async function getOwnerUserId(req) {
if (req.params.username) {
const user = await User.findOne({ username: req.params.username });
if (user && user._id) {
return user._id;
}
} else if (req.user._id) {
return req.user._id;
}
return null;
}
export default function listCollections(req, res) {
function sendFailure({ code = 500, message = 'Something went wrong' }) {
res.status(code).json({ success: false, message });
}
function sendSuccess(collections) {
res.status(200).json(collections);
}
function findCollections(owner) {
if (owner == null) {
sendFailure({ code: 404, message: 'User not found' });
}
return Collection.find({ owner })
.populate([
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
populate: {
path: 'user', select: ['username']
}
}
]);
}
return getOwnerUserId(req)
.then(findCollections)
.then(sendSuccess)
.catch(sendFailure);
}

View file

@ -0,0 +1,34 @@
import Collection from '../../models/collection';
export default function createCollection(req, res) {
const { id: collectionId } = req.params;
const owner = req.user._id;
function sendFailure({ code = 500, message = 'Something went wrong' }) {
res.status(code).json({ success: false, message });
}
function sendSuccess() {
res.status(200).json({ success: true });
}
function removeCollection(collection) {
if (collection == null) {
sendFailure({ code: 404, message: 'Not found, or you user does not own this collection' });
return null;
}
return collection.remove();
}
function findCollection() {
// Only returned if owner matches current user
return Collection.findOne({ _id: collectionId, owner });
}
return findCollection()
.then(removeCollection)
.then(sendSuccess)
.catch(sendFailure);
}

View file

@ -0,0 +1,62 @@
import Collection from '../../models/collection';
import Project from '../../models/project';
export default function addProjectToCollection(req, res) {
const owner = req.user._id;
const { id: collectionId, projectId } = req.params;
function sendFailure({ code = 500, message = 'Something went wrong' }) {
res.status(code).json({ success: false, message });
}
function sendSuccess(collection) {
res.status(200).json(collection);
}
function updateCollection(collection) {
if (collection == null) {
sendFailure({ code: 404, message: 'Collection not found' });
return null;
}
if (!collection.owner.equals(owner)) {
sendFailure({ code: 403, message: 'User does not own this collection' });
return null;
}
const project = collection.items.find(p => p.project._id === projectId);
if (project != null) {
project.remove();
return collection.save();
}
const error = new Error('not found');
error.code = 404;
throw error;
}
function populateReferences(collection) {
return Collection.populate(
collection,
[
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
populate: {
path: 'user', select: ['username']
}
}
]
);
}
return Collection.findById(collectionId)
.populate('items.project', '_id')
.then(updateCollection)
.then(populateReferences)
.then(sendSuccess)
.catch(sendFailure);
}

View file

@ -0,0 +1,54 @@
import omitBy from 'lodash/omitBy';
import isUndefined from 'lodash/isUndefined';
import Collection from '../../models/collection';
function removeUndefined(obj) {
return omitBy(obj, isUndefined);
}
export default function createCollection(req, res) {
const { id: collectionId } = req.params;
const owner = req.user._id;
const { name, description, slug } = req.body;
const values = removeUndefined({
name,
description,
slug
});
function sendFailure({ code = 500, message = 'Something went wrong' }) {
res.status(code).json({ success: false, message });
}
function sendSuccess(collection) {
if (collection == null) {
sendFailure({ code: 404, message: 'Not found, or you user does not own this collection' });
return;
}
res.json(collection);
}
async function findAndUpdateCollection() {
// Only update if owner matches current user
return Collection.findOneAndUpdate(
{ _id: collectionId, owner },
values,
{ new: true, runValidators: true, setDefaultsOnInsert: true }
).populate([
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
populate: {
path: 'user', select: ['username']
}
}
]).exec();
}
return findAndUpdateCollection()
.then(sendSuccess)
.catch(sendFailure);
}

View file

@ -0,0 +1,48 @@
import mongoose from 'mongoose';
import shortid from 'shortid';
import slugify from 'slugify';
const { Schema } = mongoose;
const collectedProjectSchema = new Schema(
{
project: { type: Schema.Types.ObjectId, ref: 'Project' },
},
{ timestamps: true, _id: true, usePushEach: true }
);
collectedProjectSchema.virtual('id').get(function getId() {
return this._id.toHexString();
});
collectedProjectSchema.set('toJSON', {
virtuals: true
});
const collectionSchema = new Schema(
{
_id: { type: String, default: shortid.generate },
name: { type: String, default: 'My collection' },
description: { type: String },
slug: { type: String },
owner: { type: Schema.Types.ObjectId, ref: 'User' },
items: { type: [collectedProjectSchema] }
},
{ timestamps: true, usePushEach: true }
);
collectionSchema.virtual('id').get(function getId() {
return this._id;
});
collectionSchema.set('toJSON', {
virtuals: true
});
collectionSchema.pre('save', function generateSlug(next) {
const collection = this;
collection.slug = slugify(collection.name, '_');
return next();
});
export default mongoose.model('Collection', collectionSchema);

View file

@ -0,0 +1,20 @@
import { Router } from 'express';
import * as CollectionController from '../controllers/collection.controller';
import isAuthenticated from '../utils/isAuthenticated';
const router = new Router();
// List collections
router.get('/collections', isAuthenticated, CollectionController.listCollections);
router.get('/:username/collections', CollectionController.listCollections);
// Create, modify, delete collection
router.post('/collections', isAuthenticated, CollectionController.createCollection);
router.patch('/collections/:id', isAuthenticated, CollectionController.updateCollection);
router.delete('/collections/:id', isAuthenticated, CollectionController.removeCollection);
// Add and remove projects to collection
router.post('/collections/:id/:projectId', isAuthenticated, CollectionController.addProjectToCollection);
router.delete('/collections/:id/:projectId', isAuthenticated, CollectionController.removeProjectFromCollection);
export default router;

View file

@ -21,6 +21,7 @@ import users from './routes/user.routes';
import sessions from './routes/session.routes';
import projects from './routes/project.routes';
import files from './routes/file.routes';
import collections from './routes/collection.routes';
import aws from './routes/aws.routes';
import serverRoutes from './routes/server.routes';
import embedRoutes from './routes/embed.routes';
@ -102,6 +103,7 @@ app.use('/editor', requestsOfTypeJSON(), sessions);
app.use('/editor', requestsOfTypeJSON(), files);
app.use('/editor', requestsOfTypeJSON(), projects);
app.use('/editor', requestsOfTypeJSON(), aws);
app.use('/editor', requestsOfTypeJSON(), collections);
// This is a temporary way to test access via Personal Access Tokens
// Sending a valid username:<personal-access-token> combination will