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:
parent
0ae7a9eebb
commit
83978acc1d
10 changed files with 395 additions and 0 deletions
|
@ -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);
|
||||
}
|
47
server/controllers/collection.controller/createCollection.js
Normal file
47
server/controllers/collection.controller/createCollection.js
Normal 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);
|
||||
}
|
6
server/controllers/collection.controller/index.js
Normal file
6
server/controllers/collection.controller/index.js
Normal 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';
|
48
server/controllers/collection.controller/listCollections.js
Normal file
48
server/controllers/collection.controller/listCollections.js
Normal 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);
|
||||
}
|
34
server/controllers/collection.controller/removeCollection.js
Normal file
34
server/controllers/collection.controller/removeCollection.js
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
54
server/controllers/collection.controller/updateCollection.js
Normal file
54
server/controllers/collection.controller/updateCollection.js
Normal 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);
|
||||
}
|
48
server/models/collection.js
Normal file
48
server/models/collection.js
Normal 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);
|
20
server/routes/collection.routes.js
Normal file
20
server/routes/collection.routes.js
Normal 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;
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue