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 sessions from './routes/session.routes';
|
||||||
import projects from './routes/project.routes';
|
import projects from './routes/project.routes';
|
||||||
import files from './routes/file.routes';
|
import files from './routes/file.routes';
|
||||||
|
import collections from './routes/collection.routes';
|
||||||
import aws from './routes/aws.routes';
|
import aws from './routes/aws.routes';
|
||||||
import serverRoutes from './routes/server.routes';
|
import serverRoutes from './routes/server.routes';
|
||||||
import embedRoutes from './routes/embed.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(), files);
|
||||||
app.use('/editor', requestsOfTypeJSON(), projects);
|
app.use('/editor', requestsOfTypeJSON(), projects);
|
||||||
app.use('/editor', requestsOfTypeJSON(), aws);
|
app.use('/editor', requestsOfTypeJSON(), aws);
|
||||||
|
app.use('/editor', requestsOfTypeJSON(), collections);
|
||||||
|
|
||||||
// This is a temporary way to test access via Personal Access Tokens
|
// This is a temporary way to test access via Personal Access Tokens
|
||||||
// Sending a valid username:<personal-access-token> combination will
|
// Sending a valid username:<personal-access-token> combination will
|
||||||
|
|
Loading…
Reference in a new issue